pi-smart-compact 8.0.5 → 8.0.7
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/ARCHITECTURE.md +513 -0
- package/CHANGELOG.md +29 -0
- package/README.md +99 -68
- package/SECURITY.md +8 -3
- package/dist/app/mode-policy.d.ts +0 -1
- package/dist/app/mode-policy.d.ts.map +1 -1
- package/dist/app/preflight.d.ts +16 -2
- package/dist/app/preflight.d.ts.map +1 -1
- 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/state.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/app/steps/verify.d.ts.map +1 -1
- package/dist/app/steps/window.d.ts +3 -1
- package/dist/app/steps/window.d.ts.map +1 -1
- package/dist/constants.d.ts +18 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/domain/telemetry.d.ts +3 -0
- package/dist/domain/telemetry.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +969 -652
- package/dist/infra/context-graph.d.ts.map +1 -1
- package/dist/infra/fs.d.ts +1 -1
- package/dist/infra/fs.d.ts.map +1 -1
- package/dist/infra/git.d.ts.map +1 -1
- package/dist/infra/session-identity.d.ts +4 -0
- package/dist/infra/session-identity.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/provider-eval.js +113 -22
- package/dist/provider-scenario-eval.js +151 -108
- package/dist/telemetry-report.js +138 -41
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/dashboard-insights.d.ts.map +1 -1
- package/dist/ui/overlays.d.ts +1 -1
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +5 -0
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/fingerprint.d.ts +1 -1
- package/dist/utils/fingerprint.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/state.d.ts +4 -3
- package/dist/utils/state.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts.map +1 -1
- package/docs/MIGRATING_TO_V8.md +32 -14
- package/docs/RELEASE.md +13 -10
- package/package.json +2 -1
- package/dist/app/explore-wrap.d.ts +0 -8
- package/dist/app/explore-wrap.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Type as Type2 } from "typebox";
|
|
|
9
9
|
// src/app/mode-policy.ts
|
|
10
10
|
var MODE_POLICIES = {
|
|
11
11
|
fast: {
|
|
12
|
-
profile: "
|
|
12
|
+
profile: "aggressive",
|
|
13
13
|
maxLlmCalls: 3,
|
|
14
14
|
maxInputTokens: 1e5,
|
|
15
15
|
maxOutputTokens: 20000,
|
|
@@ -17,8 +17,7 @@ var MODE_POLICIES = {
|
|
|
17
17
|
allowLlmPatch: false,
|
|
18
18
|
singlePassMultiplier: 2,
|
|
19
19
|
batchOutput: { min: 800, perChunk: 160, max: 2400 },
|
|
20
|
-
|
|
21
|
-
targetContextPercent: 45
|
|
20
|
+
targetContextPercent: 30
|
|
22
21
|
},
|
|
23
22
|
balanced: {
|
|
24
23
|
profile: "balanced",
|
|
@@ -29,21 +28,8 @@ var MODE_POLICIES = {
|
|
|
29
28
|
allowLlmPatch: false,
|
|
30
29
|
singlePassMultiplier: 1.5,
|
|
31
30
|
batchOutput: { min: 1000, perChunk: 250, max: 4096 },
|
|
32
|
-
softLatencyMs: 60000,
|
|
33
31
|
targetContextPercent: 40
|
|
34
32
|
},
|
|
35
|
-
aggressive: {
|
|
36
|
-
profile: "aggressive",
|
|
37
|
-
maxLlmCalls: 4,
|
|
38
|
-
maxInputTokens: 120000,
|
|
39
|
-
maxOutputTokens: 25000,
|
|
40
|
-
explore: false,
|
|
41
|
-
allowLlmPatch: false,
|
|
42
|
-
singlePassMultiplier: 2,
|
|
43
|
-
batchOutput: { min: 800, perChunk: 180, max: 2400 },
|
|
44
|
-
softLatencyMs: 45000,
|
|
45
|
-
targetContextPercent: 30
|
|
46
|
-
},
|
|
47
33
|
thorough: {
|
|
48
34
|
profile: "light",
|
|
49
35
|
maxLlmCalls: 8,
|
|
@@ -53,18 +39,17 @@ var MODE_POLICIES = {
|
|
|
53
39
|
allowLlmPatch: true,
|
|
54
40
|
singlePassMultiplier: 0.9,
|
|
55
41
|
batchOutput: { min: 1500, perChunk: 400, max: 6000 },
|
|
56
|
-
softLatencyMs: 120000,
|
|
57
42
|
targetContextPercent: 50
|
|
58
43
|
}
|
|
59
44
|
};
|
|
60
45
|
function modeFromLegacyProfile(profile) {
|
|
61
|
-
return profile === "light" ? "thorough" : profile;
|
|
46
|
+
return profile === "light" ? "thorough" : profile === "aggressive" ? "fast" : "balanced";
|
|
62
47
|
}
|
|
63
48
|
function resolveMode(requested, contextPercent, extraction, additionalRisk = 0) {
|
|
64
49
|
if (requested !== "auto")
|
|
65
|
-
return requested;
|
|
50
|
+
return requested === "aggressive" ? "fast" : requested;
|
|
66
51
|
if (contextPercent >= 85)
|
|
67
|
-
return "
|
|
52
|
+
return "fast";
|
|
68
53
|
if (!extraction)
|
|
69
54
|
return contextPercent < 70 ? "fast" : "balanced";
|
|
70
55
|
const unresolved = extraction.errors.filter((error) => !error.resolved).length;
|
|
@@ -117,10 +102,16 @@ function effectiveBudget(configured, modeDefault) {
|
|
|
117
102
|
}
|
|
118
103
|
|
|
119
104
|
// src/constants.ts
|
|
120
|
-
var VERSION = "8.0.
|
|
105
|
+
var VERSION = "8.0.7";
|
|
121
106
|
var CHARS_PER_TOKEN = 3.8;
|
|
122
107
|
var MIN_COMPACTION_SAVING_RATIO = 0.1;
|
|
123
108
|
var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
|
|
109
|
+
var POST_SUMMARY_RESERVE_RATIO = 0.25;
|
|
110
|
+
var BUDGET_LIMITS = {
|
|
111
|
+
CALLS: { min: 1, max: 100 },
|
|
112
|
+
INPUT_TOKENS: { min: 1e4, max: 1e6 },
|
|
113
|
+
LATENCY_MS: { min: 5000, max: 600000 }
|
|
114
|
+
};
|
|
124
115
|
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.";
|
|
125
116
|
var PROFILES = {
|
|
126
117
|
light: {
|
|
@@ -492,13 +483,8 @@ var LOCK_STALE_MS = 5000;
|
|
|
492
483
|
var LOCK_RETRY_MS = 25;
|
|
493
484
|
var LOCK_MAX_RETRIES = 80;
|
|
494
485
|
function ensureDir(dir) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
} catch (e) {
|
|
498
|
-
if (e?.code !== "EEXIST") {
|
|
499
|
-
warn("ensureDir failed for " + dir, e);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
486
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
487
|
+
fs.chmodSync(dir, 448);
|
|
502
488
|
}
|
|
503
489
|
function tempPath(target) {
|
|
504
490
|
return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
@@ -507,8 +493,9 @@ function atomicWriteFileSync(target, data) {
|
|
|
507
493
|
ensureDir(path2.dirname(target));
|
|
508
494
|
const tmp = tempPath(target);
|
|
509
495
|
try {
|
|
510
|
-
fs.writeFileSync(tmp, data);
|
|
496
|
+
fs.writeFileSync(tmp, data, { mode: 384 });
|
|
511
497
|
fs.renameSync(tmp, target);
|
|
498
|
+
fs.chmodSync(target, 384);
|
|
512
499
|
} catch (e) {
|
|
513
500
|
try {
|
|
514
501
|
fs.unlinkSync(tmp);
|
|
@@ -520,7 +507,7 @@ function acquireLockSync(target) {
|
|
|
520
507
|
const lockDir = target + ".lock";
|
|
521
508
|
for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
522
509
|
try {
|
|
523
|
-
fs.mkdirSync(lockDir);
|
|
510
|
+
fs.mkdirSync(lockDir, { mode: 448 });
|
|
524
511
|
return () => {
|
|
525
512
|
try {
|
|
526
513
|
fs.rmdirSync(lockDir);
|
|
@@ -562,9 +549,12 @@ function appendLineLocked(target, line) {
|
|
|
562
549
|
ensureDir(path2.dirname(target));
|
|
563
550
|
const release = acquireLockSync(target);
|
|
564
551
|
try {
|
|
552
|
+
if (fs.existsSync(target))
|
|
553
|
+
fs.chmodSync(target, 384);
|
|
565
554
|
fs.appendFileSync(target, line.endsWith(`
|
|
566
555
|
`) ? line : line + `
|
|
567
|
-
|
|
556
|
+
`, { mode: 384 });
|
|
557
|
+
fs.chmodSync(target, 384);
|
|
568
558
|
} finally {
|
|
569
559
|
release();
|
|
570
560
|
}
|
|
@@ -653,6 +643,27 @@ function writeJsonSync(target, value, pretty = false) {
|
|
|
653
643
|
// src/utils/extraction.ts
|
|
654
644
|
import path3 from "path";
|
|
655
645
|
|
|
646
|
+
// src/utils/lru.ts
|
|
647
|
+
function lruGet(m, key) {
|
|
648
|
+
if (!m.has(key))
|
|
649
|
+
return;
|
|
650
|
+
const v = m.get(key);
|
|
651
|
+
m.delete(key);
|
|
652
|
+
m.set(key, v);
|
|
653
|
+
return v;
|
|
654
|
+
}
|
|
655
|
+
function lruSet(m, key, value, max) {
|
|
656
|
+
if (m.has(key))
|
|
657
|
+
m.delete(key);
|
|
658
|
+
m.set(key, value);
|
|
659
|
+
while (m.size > max) {
|
|
660
|
+
const oldest = m.keys().next().value;
|
|
661
|
+
if (oldest === undefined)
|
|
662
|
+
break;
|
|
663
|
+
m.delete(oldest);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
656
667
|
// src/utils/tokens.ts
|
|
657
668
|
var PROVIDER_MAP = {
|
|
658
669
|
"zai-anthropic": {
|
|
@@ -850,35 +861,19 @@ class TokenCalibrationStore {
|
|
|
850
861
|
if (!provider)
|
|
851
862
|
return 1;
|
|
852
863
|
const exactKey = calibrationKey(provider, model);
|
|
853
|
-
const exact = this.factors
|
|
854
|
-
if (exact !== undefined)
|
|
855
|
-
this.factors.delete(exactKey);
|
|
856
|
-
this.factors.set(exactKey, exact);
|
|
864
|
+
const exact = lruGet(this.factors, exactKey);
|
|
865
|
+
if (exact !== undefined)
|
|
857
866
|
return exact;
|
|
858
|
-
|
|
859
|
-
const providerKey = calibrationKey(provider);
|
|
860
|
-
const fallback = this.factors.get(providerKey);
|
|
861
|
-
if (fallback !== undefined) {
|
|
862
|
-
this.factors.delete(providerKey);
|
|
863
|
-
this.factors.set(providerKey, fallback);
|
|
864
|
-
}
|
|
865
|
-
return fallback ?? 1;
|
|
867
|
+
return lruGet(this.factors, calibrationKey(provider)) ?? 1;
|
|
866
868
|
}
|
|
867
869
|
calibrate(estimated, actual, provider, model) {
|
|
868
870
|
if (actual <= 0 || estimated <= 0 || !provider)
|
|
869
871
|
return;
|
|
870
872
|
const key = calibrationKey(provider, model);
|
|
871
|
-
const prev = this.factors
|
|
873
|
+
const prev = lruGet(this.factors, key) ?? 1;
|
|
872
874
|
const target = prev * actual / estimated;
|
|
873
875
|
const clamped = Math.max(TUNING.CALIBRATION_CLAMP_MIN, Math.min(TUNING.CALIBRATION_CLAMP_MAX, target));
|
|
874
|
-
this.factors.
|
|
875
|
-
this.factors.set(key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE);
|
|
876
|
-
while (this.factors.size > Math.max(1, this.maxEntries)) {
|
|
877
|
-
const oldest = this.factors.keys().next().value;
|
|
878
|
-
if (oldest === undefined)
|
|
879
|
-
break;
|
|
880
|
-
this.factors.delete(oldest);
|
|
881
|
-
}
|
|
876
|
+
lruSet(this.factors, key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE, Math.max(1, this.maxEntries));
|
|
882
877
|
}
|
|
883
878
|
size() {
|
|
884
879
|
return this.factors.size;
|
|
@@ -1218,12 +1213,192 @@ function extractFileRefs(summary) {
|
|
|
1218
1213
|
return candidates.filter(isLikelyFileRef);
|
|
1219
1214
|
}
|
|
1220
1215
|
|
|
1216
|
+
// src/domain/summary-schema.ts
|
|
1217
|
+
function classifyHeading(raw) {
|
|
1218
|
+
const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
|
|
1219
|
+
if (!text)
|
|
1220
|
+
return "unknown";
|
|
1221
|
+
if (text === "goal" || text === "goals" || text === "objective" || text === "objectives")
|
|
1222
|
+
return "goal";
|
|
1223
|
+
if (text.startsWith("constraint") || text.includes("preference"))
|
|
1224
|
+
return "constraints";
|
|
1225
|
+
if (text === "progress" || text === "status")
|
|
1226
|
+
return "progress";
|
|
1227
|
+
if (text.includes("key decision") || text === "decisions")
|
|
1228
|
+
return "decisions";
|
|
1229
|
+
if (text.includes("file") && text.includes("modif"))
|
|
1230
|
+
return "files-modified";
|
|
1231
|
+
if (text.includes("file") && (text.includes("read") || text.includes("viewed")))
|
|
1232
|
+
return "files-read";
|
|
1233
|
+
if (text.includes("next step") || text === "next actions")
|
|
1234
|
+
return "next-steps";
|
|
1235
|
+
if (text.includes("critical context") || text === "important context")
|
|
1236
|
+
return "critical-context";
|
|
1237
|
+
if (text === "topics" || text.includes("topics covered"))
|
|
1238
|
+
return "topics";
|
|
1239
|
+
if (text.includes("open loop") || text.includes("unresolved"))
|
|
1240
|
+
return "open-loops";
|
|
1241
|
+
if (text.includes("changes since") || text === "changes")
|
|
1242
|
+
return "changes";
|
|
1243
|
+
if (text.includes("verification"))
|
|
1244
|
+
return "verification-note";
|
|
1245
|
+
return "unknown";
|
|
1246
|
+
}
|
|
1247
|
+
function canonicalHeading(kind) {
|
|
1248
|
+
switch (kind) {
|
|
1249
|
+
case "goal":
|
|
1250
|
+
return SECTION_GOAL;
|
|
1251
|
+
case "constraints":
|
|
1252
|
+
return SECTION_CONSTRAINTS;
|
|
1253
|
+
case "progress":
|
|
1254
|
+
return SECTION_PROGRESS;
|
|
1255
|
+
case "decisions":
|
|
1256
|
+
return SECTION_DECISIONS;
|
|
1257
|
+
case "files-modified":
|
|
1258
|
+
return SECTION_FILES_MODIFIED;
|
|
1259
|
+
case "files-read":
|
|
1260
|
+
return SECTION_FILES_READ;
|
|
1261
|
+
case "next-steps":
|
|
1262
|
+
return SECTION_NEXT_STEPS;
|
|
1263
|
+
case "critical-context":
|
|
1264
|
+
return SECTION_CRITICAL_CONTEXT;
|
|
1265
|
+
case "topics":
|
|
1266
|
+
return SECTION_TOPICS;
|
|
1267
|
+
case "open-loops":
|
|
1268
|
+
return SECTION_OPEN_LOOPS;
|
|
1269
|
+
case "changes":
|
|
1270
|
+
return SECTION_CHANGES;
|
|
1271
|
+
case "verification-note":
|
|
1272
|
+
return "## Verification Note";
|
|
1273
|
+
case "unknown":
|
|
1274
|
+
default:
|
|
1275
|
+
return "## Section";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/domain/summary-parse.ts
|
|
1280
|
+
var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
1281
|
+
function summaryEvidenceLine(value, maxLength) {
|
|
1282
|
+
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
|
|
1283
|
+
}
|
|
1284
|
+
function mergeBodies(first, second) {
|
|
1285
|
+
const seen = new Set;
|
|
1286
|
+
return [first, second].filter(Boolean).flatMap((body) => body.split(`
|
|
1287
|
+
`)).filter((line) => seen.has(line) ? false : (seen.add(line), true)).join(`
|
|
1288
|
+
`).trim();
|
|
1289
|
+
}
|
|
1290
|
+
function parseSummary(markdown) {
|
|
1291
|
+
const sections = [];
|
|
1292
|
+
const lines = markdown.split(`
|
|
1293
|
+
`);
|
|
1294
|
+
let currentHeading = "";
|
|
1295
|
+
let currentKind = "unknown";
|
|
1296
|
+
let bodyLines = [];
|
|
1297
|
+
let started = false;
|
|
1298
|
+
const flush = () => {
|
|
1299
|
+
if (!started)
|
|
1300
|
+
return;
|
|
1301
|
+
const body = bodyLines.join(`
|
|
1302
|
+
`).trim();
|
|
1303
|
+
const existing = currentKind === "unknown" ? undefined : sections.find((s) => s.kind === currentKind);
|
|
1304
|
+
if (existing)
|
|
1305
|
+
existing.body = mergeBodies(existing.body, body);
|
|
1306
|
+
else
|
|
1307
|
+
sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
|
|
1308
|
+
};
|
|
1309
|
+
for (const line of lines) {
|
|
1310
|
+
const m = line.match(HEADING_RE);
|
|
1311
|
+
if (m) {
|
|
1312
|
+
const kind = classifyHeading(m[2]);
|
|
1313
|
+
if (m[1].length <= 2 || kind !== "unknown") {
|
|
1314
|
+
flush();
|
|
1315
|
+
currentHeading = "## " + m[2].trim();
|
|
1316
|
+
currentKind = kind;
|
|
1317
|
+
bodyLines = [];
|
|
1318
|
+
started = true;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (started)
|
|
1323
|
+
bodyLines.push(line);
|
|
1324
|
+
}
|
|
1325
|
+
flush();
|
|
1326
|
+
return { sections };
|
|
1327
|
+
}
|
|
1328
|
+
function findSection(summary, kind) {
|
|
1329
|
+
const parsed = typeof summary === "string" ? parseSummary(summary) : summary;
|
|
1330
|
+
return parsed.sections.find((s) => s.kind === kind);
|
|
1331
|
+
}
|
|
1332
|
+
function renderSummary(summary, opts = {}) {
|
|
1333
|
+
return summary.sections.map((s) => {
|
|
1334
|
+
const heading = opts.canonicalHeadings && s.kind !== "unknown" ? canonicalHeading(s.kind) : s.heading;
|
|
1335
|
+
return heading + `
|
|
1336
|
+
` + s.body;
|
|
1337
|
+
}).join(`
|
|
1338
|
+
|
|
1339
|
+
`).replace(/\n{3,}/g, `
|
|
1340
|
+
|
|
1341
|
+
`).trim() + `
|
|
1342
|
+
`;
|
|
1343
|
+
}
|
|
1344
|
+
function upsertSection(summary, kind, body, placement) {
|
|
1345
|
+
const heading = canonicalHeading(kind);
|
|
1346
|
+
const existing = summary.sections.findIndex((s) => s.kind === kind);
|
|
1347
|
+
if (existing >= 0) {
|
|
1348
|
+
const sections = summary.sections.slice();
|
|
1349
|
+
sections[existing] = { kind, heading, body: body.trim() };
|
|
1350
|
+
return { sections };
|
|
1351
|
+
}
|
|
1352
|
+
const hint = placement == null ? {} : typeof placement === "string" ? { before: placement } : placement;
|
|
1353
|
+
const section = { kind, heading, body: body.trim() };
|
|
1354
|
+
if (hint.before) {
|
|
1355
|
+
const idx = summary.sections.findIndex((s) => s.kind === hint.before);
|
|
1356
|
+
if (idx >= 0) {
|
|
1357
|
+
const sections = summary.sections.slice();
|
|
1358
|
+
sections.splice(idx, 0, section);
|
|
1359
|
+
return { sections };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (hint.after) {
|
|
1363
|
+
let idx = -1;
|
|
1364
|
+
for (let i = summary.sections.length - 1;i >= 0; i--) {
|
|
1365
|
+
if (summary.sections[i].kind === hint.after) {
|
|
1366
|
+
idx = i;
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (idx >= 0) {
|
|
1371
|
+
const sections = summary.sections.slice();
|
|
1372
|
+
sections.splice(idx + 1, 0, section);
|
|
1373
|
+
return { sections };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return { sections: [...summary.sections, section] };
|
|
1377
|
+
}
|
|
1378
|
+
function appendToSection(summary, kind, text, fallbackBody = "") {
|
|
1379
|
+
const heading = canonicalHeading(kind);
|
|
1380
|
+
const idx = summary.sections.findIndex((s) => s.kind === kind);
|
|
1381
|
+
if (idx >= 0) {
|
|
1382
|
+
const sections = summary.sections.slice();
|
|
1383
|
+
const existing = sections[idx];
|
|
1384
|
+
const body = /^-\s*(?:none|none recorded|no blockers?|yok)[.!]?$/i.test(existing.body.trim()) ? "" : existing.body.trim();
|
|
1385
|
+
const combined = body ? body + `
|
|
1386
|
+
` + text.trim() : text.trim();
|
|
1387
|
+
sections[idx] = { kind, heading, body: combined };
|
|
1388
|
+
return { sections };
|
|
1389
|
+
}
|
|
1390
|
+
return upsertSection(summary, kind, (fallbackBody.trim() ? fallbackBody.trim() + `
|
|
1391
|
+
` : "") + text.trim());
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1221
1394
|
// src/utils/extraction.ts
|
|
1222
1395
|
var TRUNCATE_RE = /\u2026\u2702\d+$/;
|
|
1223
1396
|
function isTruncated(content) {
|
|
1224
1397
|
return TRUNCATE_RE.test(extractText(content));
|
|
1225
1398
|
}
|
|
1226
|
-
|
|
1399
|
+
function nestedToolCallId(wrapperId, messageIndex, toolIndex, nestedId) {
|
|
1400
|
+
return typeof nestedId === "string" ? nestedId : wrapperId ? wrapperId + "_" + toolIndex : ID_PREFIX.MULTI_TOOL_USE_SYNTHETIC + messageIndex + "_" + toolIndex;
|
|
1401
|
+
}
|
|
1227
1402
|
function flattenToolCallBlock(b) {
|
|
1228
1403
|
if (!isToolCallBlock(b))
|
|
1229
1404
|
return [];
|
|
@@ -1304,8 +1479,8 @@ function buildToolCallIndex(msgs) {
|
|
|
1304
1479
|
const nested = flattenToolCallBlock(b);
|
|
1305
1480
|
for (let t = 0;t < nested.length; t++) {
|
|
1306
1481
|
const tool = nested[t];
|
|
1307
|
-
const
|
|
1308
|
-
idx.set(
|
|
1482
|
+
const id = nestedToolCallId(b.id, i, t, tool.id);
|
|
1483
|
+
idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
|
|
1309
1484
|
}
|
|
1310
1485
|
}
|
|
1311
1486
|
}
|
|
@@ -1333,14 +1508,15 @@ function trackFileOps(msgs, _tcIdx) {
|
|
|
1333
1508
|
if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
|
|
1334
1509
|
const existing = modMap.get(filePath);
|
|
1335
1510
|
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1511
|
+
delSet.delete(filePath);
|
|
1336
1512
|
}
|
|
1337
1513
|
} else if (operation === "delete") {
|
|
1338
1514
|
delSet.add(filePath);
|
|
1515
|
+
modMap.delete(filePath);
|
|
1516
|
+
readSet.delete(filePath);
|
|
1339
1517
|
} else if (operation === "read" || operation === "search" || operation === "list") {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
else
|
|
1343
|
-
readSet.add(filePath);
|
|
1518
|
+
readSet.add(filePath);
|
|
1519
|
+
delSet.delete(filePath);
|
|
1344
1520
|
}
|
|
1345
1521
|
}
|
|
1346
1522
|
return {
|
|
@@ -1366,6 +1542,10 @@ function hasCommandFailureSignal(text) {
|
|
|
1366
1542
|
const firstLine = text.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
1367
1543
|
return LIKELY_ERROR_RE.test(firstLine) || /^(?:npm\s+error|fatal:|traceback\b)/i.test(firstLine);
|
|
1368
1544
|
}
|
|
1545
|
+
function isTransientToolDiagnostic(text) {
|
|
1546
|
+
const candidate = text.trim();
|
|
1547
|
+
return /\bBrave Search API error\s*\(429\)/i.test(candidate) || /\bnpm error code ENOLOCK\b/i.test(candidate) && /(?:audit|existing lockfile|loadVirtual)/i.test(candidate) || /^Found \d+ occurrences? of edits\[\d+\](?!\w)/i.test(candidate) || /^Unknown JSON field:/i.test(candidate) && /Available fields:/i.test(candidate);
|
|
1548
|
+
}
|
|
1369
1549
|
function catalogErrors(msgs, _tcIdx) {
|
|
1370
1550
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
1371
1551
|
const errors = [];
|
|
@@ -1375,7 +1555,7 @@ function catalogErrors(msgs, _tcIdx) {
|
|
|
1375
1555
|
continue;
|
|
1376
1556
|
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
1377
1557
|
const text = extractText(m.content);
|
|
1378
|
-
if (tc && isBenignSearchResult(tc, text))
|
|
1558
|
+
if (tc && isBenignSearchResult(tc, text) || isTransientToolDiagnostic(text))
|
|
1379
1559
|
continue;
|
|
1380
1560
|
if (m.isError) {
|
|
1381
1561
|
errors.push({ index: i, tool: tc?.name ?? "unknown", message: text.slice(0, TRUNC.ERROR_DETAIL), retryAttempted: false, resolved: false });
|
|
@@ -1562,13 +1742,27 @@ function buildTimeline(msgs, errors) {
|
|
|
1562
1742
|
...timeline.filter((t) => t.event === "error")
|
|
1563
1743
|
].sort((a, b) => a.index - b.index) : timeline;
|
|
1564
1744
|
}
|
|
1745
|
+
var HISTORY_SUMMARY_RE = /^(?:The conversation history before this point was compacted|The following is a summary of a branch that this conversation came back from)[\s\S]*<summary>/i;
|
|
1746
|
+
var ACK_ONLY_RE = /^(?:(?:ok(?:ay)?|tamam|evet|yes|thanks?|te\u015Fekk\u00FCrler|continue|devam(?:\s+et)?|go\s+ahead|proceed)[\s.!]*){1,3}$/iu;
|
|
1747
|
+
function isCompactionStatusText(text) {
|
|
1748
|
+
const candidate = summaryEvidenceLine(text, TRUNC.MESSAGE).replace(/^["'`]+/, "").trim();
|
|
1749
|
+
return /^(?:EESV Compact\b|Smart compact (?:skipped|prepared|run finished)\b|Auto-compacting\b)/i.test(candidate);
|
|
1750
|
+
}
|
|
1565
1751
|
function extractMainGoal(msgs) {
|
|
1566
|
-
for (
|
|
1752
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
1753
|
+
const m = msgs[i];
|
|
1567
1754
|
if (m?.role !== "user")
|
|
1568
1755
|
continue;
|
|
1569
|
-
const
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1756
|
+
const text = extractText(m.content).trim();
|
|
1757
|
+
if (!text)
|
|
1758
|
+
continue;
|
|
1759
|
+
if (HISTORY_SUMMARY_RE.test(text)) {
|
|
1760
|
+
const carried = summaryEvidenceLine(findSection(text, "goal")?.body ?? "", TRUNC.MESSAGE);
|
|
1761
|
+
return carried && !isCompactionStatusText(carried) ? carried : null;
|
|
1762
|
+
}
|
|
1763
|
+
if (text.startsWith("/") || ACK_ONLY_RE.test(text))
|
|
1764
|
+
continue;
|
|
1765
|
+
return summaryEvidenceLine(text, TRUNC.MESSAGE) || null;
|
|
1572
1766
|
}
|
|
1573
1767
|
return null;
|
|
1574
1768
|
}
|
|
@@ -1687,12 +1881,16 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1687
1881
|
|
|
1688
1882
|
// src/utils/helpers.ts
|
|
1689
1883
|
var VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
1690
|
-
var VALID_MODES = ["auto", "
|
|
1884
|
+
var VALID_MODES = ["auto", "fast", "balanced", "thorough"];
|
|
1691
1885
|
var VALID_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1692
1886
|
var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
|
|
1693
1887
|
function validateSmartCompactConfig(sc) {
|
|
1888
|
+
if (sc.mode === "aggressive") {
|
|
1889
|
+
warn("smart-compact config: mode 'aggressive' is deprecated; using 'fast'.");
|
|
1890
|
+
sc.mode = "fast";
|
|
1891
|
+
}
|
|
1694
1892
|
if ("mode" in sc && !VALID_MODES.includes(sc.mode)) {
|
|
1695
|
-
warn("smart-compact config: invalid mode '" + sc.mode + "', expected auto|balanced|
|
|
1893
|
+
warn("smart-compact config: invalid mode '" + sc.mode + "', expected auto|fast|balanced|thorough. Using default 'auto'.");
|
|
1696
1894
|
delete sc.mode;
|
|
1697
1895
|
}
|
|
1698
1896
|
if ("telemetryChannel" in sc && sc.telemetryChannel !== "stable" && sc.telemetryChannel !== "canary") {
|
|
@@ -3086,12 +3284,22 @@ function mergeExtractions(base, delta, baseMsgCount, deltaMessages = [], deltaTo
|
|
|
3086
3284
|
const previous = modified.get(file.path);
|
|
3087
3285
|
modified.set(file.path, previous ? { ...file, toolCalls: previous.toolCalls + file.toolCalls, lastModifiedIndex: Math.max(previous.lastModifiedIndex, file.lastModifiedIndex) } : file);
|
|
3088
3286
|
}
|
|
3287
|
+
const deltaPresent = new Set([...offsetModifiedFiles.map((file) => file.path), ...delta.readFiles]);
|
|
3288
|
+
const deltaDeleted = new Set(delta.deletedFiles);
|
|
3289
|
+
for (const file of deltaDeleted)
|
|
3290
|
+
modified.delete(file);
|
|
3291
|
+
const readFiles = new Set([...base.readFiles, ...delta.readFiles]);
|
|
3292
|
+
for (const file of deltaDeleted)
|
|
3293
|
+
readFiles.delete(file);
|
|
3294
|
+
const deletedFiles = new Set([...base.deletedFiles, ...delta.deletedFiles]);
|
|
3295
|
+
for (const file of deltaPresent)
|
|
3296
|
+
deletedFiles.delete(file);
|
|
3089
3297
|
const reconciledBaseErrors = reconcileCachedErrors(base.errors, deltaMessages, deltaToolCalls, baseMsgCount);
|
|
3090
|
-
const mergedErrors = [...reconciledBaseErrors, ...offsetErrors];
|
|
3298
|
+
const mergedErrors = [...reconciledBaseErrors, ...offsetErrors].filter((error2) => !isTransientToolDiagnostic(error2.message));
|
|
3091
3299
|
return {
|
|
3092
3300
|
modifiedFiles: [...modified.values()],
|
|
3093
|
-
readFiles: [...
|
|
3094
|
-
deletedFiles: [...
|
|
3301
|
+
readFiles: [...readFiles],
|
|
3302
|
+
deletedFiles: [...deletedFiles],
|
|
3095
3303
|
referencedFiles: [...new Set([...base.referencedFiles ?? [], ...delta.referencedFiles ?? []])].slice(0, 200),
|
|
3096
3304
|
mediaAttachments: [...base.mediaAttachments ?? [], ...offsetMedia],
|
|
3097
3305
|
errors: mergedErrors,
|
|
@@ -3099,7 +3307,7 @@ function mergeExtractions(base, delta, baseMsgCount, deltaMessages = [], deltaTo
|
|
|
3099
3307
|
constraints: [...base.constraints, ...offsetConstraints],
|
|
3100
3308
|
topics: [...base.topics, ...offsetTopics],
|
|
3101
3309
|
timeline: [...base.timeline, ...offsetTimeline],
|
|
3102
|
-
mainGoal:
|
|
3310
|
+
mainGoal: delta.mainGoal ?? base.mainGoal,
|
|
3103
3311
|
lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
|
|
3104
3312
|
lastErrors: mergedErrors.filter((error2) => !error2.resolved).map((error2) => error2.message).slice(-3),
|
|
3105
3313
|
messageCount: baseMsgCount + delta.messageCount
|
|
@@ -3220,10 +3428,10 @@ function p95(values) {
|
|
|
3220
3428
|
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
|
|
3221
3429
|
}
|
|
3222
3430
|
function stats(entries, damage) {
|
|
3223
|
-
const
|
|
3224
|
-
const
|
|
3225
|
-
const
|
|
3226
|
-
const appliedRunIds = new Set(
|
|
3431
|
+
const evidence = entries.filter((entry) => entry.status !== "dry-run");
|
|
3432
|
+
const successfulRuns = evidence.filter((entry) => entry.status === "success");
|
|
3433
|
+
const quality = successfulRuns.filter((entry) => typeof entry.verificationScore === "number");
|
|
3434
|
+
const appliedRunIds = new Set(successfulRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
|
|
3227
3435
|
const observedScores = new Map;
|
|
3228
3436
|
for (const observation of damage) {
|
|
3229
3437
|
if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
|
|
@@ -3233,14 +3441,15 @@ function stats(entries, damage) {
|
|
|
3233
3441
|
const damaging = [...observedScores.values()].filter((score) => score > 0).length;
|
|
3234
3442
|
return {
|
|
3235
3443
|
runs: entries.length,
|
|
3236
|
-
|
|
3444
|
+
appliedRuns: evidence.length,
|
|
3445
|
+
successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
|
|
3237
3446
|
avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
|
|
3238
|
-
qualityCoverage:
|
|
3239
|
-
p95LatencyMs: p95(
|
|
3240
|
-
avgTokens:
|
|
3241
|
-
fallbackRate:
|
|
3447
|
+
qualityCoverage: evidence.length ? quality.length / evidence.length : 0,
|
|
3448
|
+
p95LatencyMs: p95(evidence.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
|
|
3449
|
+
avgTokens: evidence.length ? evidence.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / evidence.length : 0,
|
|
3450
|
+
fallbackRate: evidence.length ? evidence.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / evidence.length : 0,
|
|
3242
3451
|
damageRate: observedScores.size ? damaging / observedScores.size : 0,
|
|
3243
|
-
damageCoverage:
|
|
3452
|
+
damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
|
|
3244
3453
|
};
|
|
3245
3454
|
}
|
|
3246
3455
|
function roundStats(value) {
|
|
@@ -3265,7 +3474,7 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3265
3474
|
const triggers = [];
|
|
3266
3475
|
const failureBaseline = 1 - baseline.successRate;
|
|
3267
3476
|
const failureCanary = 1 - canary.successRate;
|
|
3268
|
-
if (canary.
|
|
3477
|
+
if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
|
|
3269
3478
|
triggers.push({
|
|
3270
3479
|
metric: "failure-rate",
|
|
3271
3480
|
baseline: failureBaseline,
|
|
@@ -3293,15 +3502,17 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3293
3502
|
if (canary.damageRate - baseline.damageRate >= 0.1) {
|
|
3294
3503
|
triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
|
|
3295
3504
|
}
|
|
3296
|
-
const
|
|
3505
|
+
const canarySampleAdequacy = Math.min(1, canary.appliedRuns / minCanaryRuns);
|
|
3506
|
+
const baselineSampleAdequacy = Math.min(1, baseline.appliedRuns / Math.max(20, minCanaryRuns));
|
|
3507
|
+
const dataConfidence = Math.round(100 * (canarySampleAdequacy * 0.25 + baselineSampleAdequacy * 0.15 + canary.qualityCoverage * canarySampleAdequacy * 0.2 + canary.damageCoverage * canarySampleAdequacy * 0.2 + baseline.damageCoverage * baselineSampleAdequacy * 0.2));
|
|
3297
3508
|
const reasons = [];
|
|
3298
3509
|
let decision = "hold";
|
|
3299
|
-
if (triggers.length && canary.
|
|
3510
|
+
if (triggers.length && canary.appliedRuns >= 3) {
|
|
3300
3511
|
decision = "rollback";
|
|
3301
3512
|
reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
|
|
3302
|
-
} else if (canary.
|
|
3303
|
-
reasons.push("need " + (minCanaryRuns - canary.
|
|
3304
|
-
} else if (baseline.
|
|
3513
|
+
} else if (canary.appliedRuns < minCanaryRuns) {
|
|
3514
|
+
reasons.push("need " + (minCanaryRuns - canary.appliedRuns) + " more canary runs with applied outcomes");
|
|
3515
|
+
} else if (baseline.appliedRuns < Math.max(20, minCanaryRuns)) {
|
|
3305
3516
|
reasons.push("stable baseline is too small");
|
|
3306
3517
|
} else if (canary.qualityCoverage < 0.7) {
|
|
3307
3518
|
reasons.push("schema-v2 quality coverage is below 70%");
|
|
@@ -3327,7 +3538,7 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3327
3538
|
reasons
|
|
3328
3539
|
};
|
|
3329
3540
|
}
|
|
3330
|
-
var
|
|
3541
|
+
var TELEMETRY_FAILURE_KINDS = new Set([
|
|
3331
3542
|
"cancelled",
|
|
3332
3543
|
"timeout",
|
|
3333
3544
|
"rate-limit",
|
|
@@ -3341,6 +3552,9 @@ var FAILURE_KINDS = new Set([
|
|
|
3341
3552
|
"yield",
|
|
3342
3553
|
"internal"
|
|
3343
3554
|
]);
|
|
3555
|
+
function isTelemetryFailureKind(value) {
|
|
3556
|
+
return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
|
|
3557
|
+
}
|
|
3344
3558
|
|
|
3345
3559
|
// src/ui/dashboard-format.ts
|
|
3346
3560
|
var DASHBOARD_PAGE_SIZE = 24;
|
|
@@ -3636,7 +3850,7 @@ function formatDashboardCanary(insights) {
|
|
|
3636
3850
|
"Canary / stable control",
|
|
3637
3851
|
"",
|
|
3638
3852
|
"Decision: " + c.decision.toUpperCase() + " | data confidence " + c.dataConfidence + "%",
|
|
3639
|
-
"Runs: stable " + c.baseline.runs + " | canary " + c.canary.runs,
|
|
3853
|
+
"Runs (total/applied): stable " + c.baseline.runs + "/" + c.baseline.appliedRuns + " | canary " + c.canary.runs + "/" + c.canary.appliedRuns,
|
|
3640
3854
|
"Success: stable " + Math.round(c.baseline.successRate * 100) + "% | canary " + Math.round(c.canary.successRate * 100) + "%",
|
|
3641
3855
|
"Quality: stable " + (c.baseline.avgQuality?.toFixed(1) ?? "\u2014") + " | canary " + (c.canary.avgQuality?.toFixed(1) ?? "\u2014"),
|
|
3642
3856
|
"p95: stable " + c.baseline.p95LatencyMs + "ms | canary " + c.canary.p95LatencyMs + "ms",
|
|
@@ -3651,21 +3865,8 @@ function formatDashboardCanary(insights) {
|
|
|
3651
3865
|
}
|
|
3652
3866
|
function buildDashboardInsights(entries, damageEntries = [], options = {}) {
|
|
3653
3867
|
const failures = {};
|
|
3654
|
-
const knownFailures = new Set([
|
|
3655
|
-
"cancelled",
|
|
3656
|
-
"timeout",
|
|
3657
|
-
"rate-limit",
|
|
3658
|
-
"authentication",
|
|
3659
|
-
"budget",
|
|
3660
|
-
"output-limit",
|
|
3661
|
-
"provider",
|
|
3662
|
-
"persistence",
|
|
3663
|
-
"validation",
|
|
3664
|
-
"verification",
|
|
3665
|
-
"internal"
|
|
3666
|
-
]);
|
|
3667
3868
|
for (const entry of entries) {
|
|
3668
|
-
if (
|
|
3869
|
+
if (isTelemetryFailureKind(entry.failureKind)) {
|
|
3669
3870
|
failures[entry.failureKind] = (failures[entry.failureKind] ?? 0) + 1;
|
|
3670
3871
|
}
|
|
3671
3872
|
}
|
|
@@ -3815,7 +4016,7 @@ function canaryRows(insights) {
|
|
|
3815
4016
|
const baseline = insights.canary.baseline;
|
|
3816
4017
|
const canary = insights.canary.canary;
|
|
3817
4018
|
const rows = [
|
|
3818
|
-
["Runs", metricNum(baseline.runs), metricNum(canary.runs)],
|
|
4019
|
+
["Runs (total/applied)", metricNum(baseline.runs) + "/" + metricNum(baseline.appliedRuns), metricNum(canary.runs) + "/" + metricNum(canary.appliedRuns)],
|
|
3819
4020
|
["Success", metricPct(baseline.successRate), metricPct(canary.successRate)],
|
|
3820
4021
|
["Verify quality", baseline.avgQuality?.toFixed(1) ?? "\u2014", canary.avgQuality?.toFixed(1) ?? "\u2014"],
|
|
3821
4022
|
["p95 duration", metricMs(baseline.p95LatencyMs), metricMs(canary.p95LatencyMs)],
|
|
@@ -3921,7 +4122,7 @@ function buildMetricsReport(entries = readMetricsLog(100), damageEntries, prebui
|
|
|
3921
4122
|
"- Repair: initial average " + (quality.averageInitial?.toFixed(1) ?? "\u2014") + " \xB7 average gain " + (quality.averageRepairGain?.toFixed(1) ?? "\u2014") + " \xB7 deterministic " + quality.deterministicPatchedRuns + " \xB7 LLM " + quality.llmPatchedRuns + " \xB7 quality floor " + quality.qualityFloorRuns + " \xB7 remaining gaps " + quality.remainingGaps,
|
|
3922
4123
|
"",
|
|
3923
4124
|
"## Canary / stable control",
|
|
3924
|
-
"Decision: " + canary.decision.toUpperCase() + " \xB7 confidence " + canary.dataConfidence + "% \xB7 stable
|
|
4125
|
+
"Decision: " + canary.decision.toUpperCase() + " \xB7 confidence " + canary.dataConfidence + "% \xB7 stable total/applied=" + canary.baseline.runs + "/" + canary.baseline.appliedRuns + " \xB7 canary total/applied=" + canary.canary.runs + "/" + canary.canary.appliedRuns,
|
|
3925
4126
|
...canary.reasons.map((item) => "- " + item),
|
|
3926
4127
|
...canary.triggers.map((item) => "- Trigger " + item.metric + ": stable " + item.baseline + " \u2192 canary " + item.canary + " (" + item.threshold + ")"),
|
|
3927
4128
|
"",
|
|
@@ -3963,7 +4164,7 @@ function writeMetricsDashboard(entries = readMetricsLog(200), damageEntries = re
|
|
|
3963
4164
|
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
3964
4165
|
${metricCard("Data Confidence", insights.confidence.score + "/100", `telemetry completeness \xB7 target \u226585 ${insights.confidence.targetMet ? "met" : "not met"}`, confidenceTone)}
|
|
3965
4166
|
${metricCard("Quality Health", insights.quality.healthScore + "/100", `actual outcomes \xB7 target \u226585 ${insights.quality.targetMet ? "met" : "not met"}`, qualityTone)}
|
|
3966
|
-
${metricCard("Canary gate", insights.canary.decision.toUpperCase(), `${insights.canary.canary.runs} canary \xB7 ${insights.canary.dataConfidence}% confidence`, canaryTone)}
|
|
4167
|
+
${metricCard("Canary gate", insights.canary.decision.toUpperCase(), `${insights.canary.canary.runs}/${insights.canary.canary.appliedRuns} canary total/applied \xB7 ${insights.canary.dataConfidence}% confidence`, canaryTone)}
|
|
3967
4168
|
</section>
|
|
3968
4169
|
<section class="layout">
|
|
3969
4170
|
<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>
|
|
@@ -4159,6 +4360,9 @@ function releaseRunLock(lock, sessionId) {
|
|
|
4159
4360
|
// src/infra/session-identity.ts
|
|
4160
4361
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4161
4362
|
var UNRESOLVED_PREFIX = "unresolved:";
|
|
4363
|
+
function branchEntryIds(branch) {
|
|
4364
|
+
return Array.from(branch, (entry) => entry.id).filter((id) => typeof id === "string");
|
|
4365
|
+
}
|
|
4162
4366
|
function resolveSessionId(ctx) {
|
|
4163
4367
|
const resolved = ctx.sessionManager?.getSessionId?.();
|
|
4164
4368
|
if (typeof resolved === "string" && resolved.length > 0)
|
|
@@ -4171,7 +4375,7 @@ function isUnresolvedSessionId(id) {
|
|
|
4171
4375
|
|
|
4172
4376
|
// src/ui/overlays.ts
|
|
4173
4377
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
4174
|
-
import { Container, Key, matchesKey, SelectList, Text, truncateToWidth,
|
|
4378
|
+
import { Container, Key, matchesKey, SelectList, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4175
4379
|
|
|
4176
4380
|
// src/utils/fingerprint.ts
|
|
4177
4381
|
import path7 from "path";
|
|
@@ -4187,7 +4391,12 @@ function findGitRoot(cwd) {
|
|
|
4187
4391
|
return ROOT_CACHE.get(cwd) ?? null;
|
|
4188
4392
|
let root = null;
|
|
4189
4393
|
try {
|
|
4190
|
-
const out = execSync("git rev-parse --show-toplevel", {
|
|
4394
|
+
const out = execSync("git rev-parse --show-toplevel", {
|
|
4395
|
+
cwd,
|
|
4396
|
+
encoding: "utf-8",
|
|
4397
|
+
timeout: 2000,
|
|
4398
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4399
|
+
});
|
|
4191
4400
|
root = out.trim() || null;
|
|
4192
4401
|
} catch (e) {
|
|
4193
4402
|
debug("git rev-parse failed for " + cwd, e);
|
|
@@ -4286,7 +4495,13 @@ function deriveFromRelativePaths(paths) {
|
|
|
4286
4495
|
return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
|
|
4287
4496
|
}
|
|
4288
4497
|
function deriveProjectIdFromCwd(cwd) {
|
|
4289
|
-
|
|
4498
|
+
if (!cwd)
|
|
4499
|
+
return null;
|
|
4500
|
+
const resolved = path7.resolve(cwd);
|
|
4501
|
+
const home2 = process.env.HOME ? path7.resolve(process.env.HOME) : null;
|
|
4502
|
+
if (resolved === path7.parse(resolved).root || resolved === home2)
|
|
4503
|
+
return null;
|
|
4504
|
+
return hashProjectId(findGitRoot2(resolved) ?? resolved);
|
|
4290
4505
|
}
|
|
4291
4506
|
function deriveProjectId(cwd, extraction, sessionId) {
|
|
4292
4507
|
if (cwd && cwd !== "/" && cwd !== process.env.HOME) {
|
|
@@ -4359,24 +4574,35 @@ function loadProjectFingerprint(projectId) {
|
|
|
4359
4574
|
}
|
|
4360
4575
|
function saveProjectFingerprint(projectId, extraction) {
|
|
4361
4576
|
try {
|
|
4362
|
-
const
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
keyDirectories
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4577
|
+
const fingerprintPath = getFingerprintPath(projectId);
|
|
4578
|
+
ensureDir(path7.dirname(fingerprintPath));
|
|
4579
|
+
const release = acquireLockSync(fingerprintPath);
|
|
4580
|
+
try {
|
|
4581
|
+
const existing = loadProjectFingerprint(projectId);
|
|
4582
|
+
const newKnownFiles = [...new Set([
|
|
4583
|
+
...existing?.knownFiles ?? [],
|
|
4584
|
+
...extraction.modifiedFiles.map((f) => f.path),
|
|
4585
|
+
...extraction.readFiles
|
|
4586
|
+
])].slice(-50);
|
|
4587
|
+
const detectedLanguage = detectLanguage(extraction);
|
|
4588
|
+
const detectedFramework = detectFramework(extraction);
|
|
4589
|
+
const keyDirectories = [...new Set([
|
|
4590
|
+
...existing?.keyDirectories ?? [],
|
|
4591
|
+
...extractKeyDirs(extraction)
|
|
4592
|
+
])].slice(-20);
|
|
4593
|
+
const fingerprint = {
|
|
4594
|
+
id: projectId,
|
|
4595
|
+
language: existing?.language && existing.language !== "unknown" ? existing.language : detectedLanguage,
|
|
4596
|
+
framework: existing?.framework ?? detectedFramework,
|
|
4597
|
+
keyDirectories,
|
|
4598
|
+
knownFiles: newKnownFiles,
|
|
4599
|
+
sessionCount: (existing?.sessionCount ?? 0) + 1,
|
|
4600
|
+
updatedAt: Date.now()
|
|
4601
|
+
};
|
|
4602
|
+
writeJsonSync(fingerprintPath, fingerprint, true);
|
|
4603
|
+
} finally {
|
|
4604
|
+
release();
|
|
4605
|
+
}
|
|
4380
4606
|
} catch (e) {
|
|
4381
4607
|
warn("saveProjectFingerprint failed", e);
|
|
4382
4608
|
}
|
|
@@ -4584,6 +4810,22 @@ function advance(rc, _stage) {
|
|
|
4584
4810
|
}
|
|
4585
4811
|
|
|
4586
4812
|
// src/app/steps/window.ts
|
|
4813
|
+
function compactionPlanReasonText(reason) {
|
|
4814
|
+
switch (reason) {
|
|
4815
|
+
case "viable":
|
|
4816
|
+
return "safe window and useful estimated saving";
|
|
4817
|
+
case "no-eligible-prefix":
|
|
4818
|
+
return "no older prefix is available";
|
|
4819
|
+
case "unsafe-tool-boundary":
|
|
4820
|
+
return "no complete tool-call boundary is available";
|
|
4821
|
+
case "retention-target-exceeded":
|
|
4822
|
+
return "a complete tool pair exceeds the tail target";
|
|
4823
|
+
case "mode-target-not-met":
|
|
4824
|
+
return "the estimated result misses this preset's target";
|
|
4825
|
+
case "insufficient-projected-saving":
|
|
4826
|
+
return "estimated saving is below 10%";
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4587
4829
|
function planCompactionWindow(input) {
|
|
4588
4830
|
const {
|
|
4589
4831
|
msgs,
|
|
@@ -4601,7 +4843,8 @@ function planCompactionWindow(input) {
|
|
|
4601
4843
|
const fixedContextTokens = overflowedContext ? 0 : Math.max(0, totalTokens - allMessageTokens);
|
|
4602
4844
|
const adaptiveKeepTokens = modelContextWindow ? Math.min(profileCfg.keepRecentTokens * 2, Math.max(profileCfg.keepRecentTokens, modelContextWindow * 0.04)) : profileCfg.keepRecentTokens;
|
|
4603
4845
|
const targetPercent = MODE_POLICIES[mode].targetContextPercent;
|
|
4604
|
-
const
|
|
4846
|
+
const postSummaryReserveTokens = Math.ceil(profileCfg.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO);
|
|
4847
|
+
const targetRetainedTokens = modelContextWindow ? Math.max(0, modelContextWindow * targetPercent / 100 - fixedContextTokens - profileCfg.summaryBudgetTokens - postSummaryReserveTokens) : adaptiveKeepTokens;
|
|
4605
4848
|
const retentionCeiling = force ? adaptiveKeepTokens : Math.max(adaptiveKeepTokens, targetRetainedTokens);
|
|
4606
4849
|
const rawMinimumTail = adaptiveKeepTokens / messageScale;
|
|
4607
4850
|
const rawRetentionCeiling = retentionCeiling / messageScale;
|
|
@@ -4644,10 +4887,10 @@ function planCompactionWindow(input) {
|
|
|
4644
4887
|
hardBoundaryAdjusted ||= keepFrom !== boundaryBeforeHardGuard;
|
|
4645
4888
|
const compactTokens = Math.round(messageTokens.slice(0, keepFrom).reduce((sum, tokens) => sum + tokens, 0) * messageScale);
|
|
4646
4889
|
const retainedTokens = retainedAt(keepFrom);
|
|
4647
|
-
const projectedAfterTokens = fixedContextTokens + retainedTokens + profileCfg.summaryBudgetTokens;
|
|
4890
|
+
const projectedAfterTokens = fixedContextTokens + retainedTokens + profileCfg.summaryBudgetTokens + postSummaryReserveTokens;
|
|
4648
4891
|
const projectedSavedTokens = Math.max(0, totalTokens - projectedAfterTokens);
|
|
4649
4892
|
const projectedYield = totalTokens > 0 ? projectedSavedTokens / totalTokens : 0;
|
|
4650
|
-
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + profileCfg.summaryBudgetTokens;
|
|
4893
|
+
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + profileCfg.summaryBudgetTokens + postSummaryReserveTokens;
|
|
4651
4894
|
let reason = "viable";
|
|
4652
4895
|
if (msgs[keepFrom]?.message?.role === "toolResult")
|
|
4653
4896
|
reason = "unsafe-tool-boundary";
|
|
@@ -4701,8 +4944,7 @@ function resolveCompactionWindow(rc) {
|
|
|
4701
4944
|
});
|
|
4702
4945
|
if (!plan.viable) {
|
|
4703
4946
|
if (rc.flags.force) {
|
|
4704
|
-
|
|
4705
|
-
rc.notify("Manual compaction skipped: " + detail, "warning");
|
|
4947
|
+
rc.notify("Manual compaction skipped: " + compactionPlanReasonText(plan.reason) + ".", "warning");
|
|
4706
4948
|
} else {
|
|
4707
4949
|
rc.notify("Smart compact skipped: the safe plan cannot meet its target; using native compaction instead.", "warning");
|
|
4708
4950
|
}
|
|
@@ -4711,281 +4953,130 @@ function resolveCompactionWindow(rc) {
|
|
|
4711
4953
|
if (overflowedContext && plan.relaxedSoftBoundaries.length) {
|
|
4712
4954
|
rc.notify("Context exceeds the active model window. EESV will summarize through soft recent-turn/checkpoint protections while preserving complete tool-call pairs; native fallback would resend the oversized context.", "warning");
|
|
4713
4955
|
}
|
|
4714
|
-
const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
|
|
4715
|
-
if (rc.flags.force && rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
|
|
4716
|
-
rc.notify("Manual compaction override at " + Math.round(contextPercent) + "% (" + totalTokens.toLocaleString() + "t): compacting about " + plan.compactTokens.toLocaleString() + "t while preserving " + plan.retainedTokens.toLocaleString() + "t of recent context. Early compaction is lossy; verification remains fail-closed.", "warning");
|
|
4717
|
-
}
|
|
4718
|
-
const out = rc;
|
|
4719
|
-
out.sessionId = resolveSessionId(rc.ctx);
|
|
4720
|
-
out.branch = branch;
|
|
4721
|
-
out.msgs = msgs;
|
|
4722
|
-
out.totalTokens = totalTokens;
|
|
4723
|
-
out.contextPercent = contextPercent;
|
|
4724
|
-
out.toolPercent = 0;
|
|
4725
|
-
out.keepFrom = plan.keepFrom;
|
|
4726
|
-
out.toCompact = msgs.slice(0, plan.keepFrom);
|
|
4727
|
-
out.firstKeptId = msgs[plan.keepFrom].id;
|
|
4728
|
-
out.compactTokens = plan.compactTokens;
|
|
4729
|
-
out.accTokens = plan.retainedTokens;
|
|
4730
|
-
out.compactionPlan = plan;
|
|
4731
|
-
return advance(out, "_windowed");
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
// src/app/preflight.ts
|
|
4735
|
-
function preflightDamageMedian(cwd, config) {
|
|
4736
|
-
if (!config.adaptiveDamageFeedback)
|
|
4737
|
-
return 0;
|
|
4738
|
-
const recent = readRecentDamageScores(deriveProjectIdFromCwd(cwd), 5).slice(-3).sort((a, b) => a - b);
|
|
4739
|
-
return recent.length ? recent[Math.floor(recent.length / 2)] : 0;
|
|
4740
|
-
}
|
|
4741
|
-
function preparePreflightProfile(input) {
|
|
4742
|
-
const config = input.config;
|
|
4743
|
-
const profile = MODE_POLICIES[input.mode].profile;
|
|
4744
|
-
let profileCfg = { ...PROFILES[profile], ...config.profiles?.[profile] ?? {} };
|
|
4745
|
-
const damageMedian = input.damageMedian ?? preflightDamageMedian(input.cwd, config);
|
|
4746
|
-
if (damageMedian >= 25) {
|
|
4747
|
-
profileCfg = {
|
|
4748
|
-
...profileCfg,
|
|
4749
|
-
keepRecentTokens: Math.round(profileCfg.keepRecentTokens * (damageMedian >= 50 ? 1.5 : 1.25)),
|
|
4750
|
-
summaryBudgetTokens: Math.round(profileCfg.summaryBudgetTokens * (damageMedian >= 50 ? 1.3 : 1.2))
|
|
4751
|
-
};
|
|
4752
|
-
}
|
|
4753
|
-
return {
|
|
4754
|
-
profileCfg,
|
|
4755
|
-
estimator: makeTokenEstimator(input.summaryModel.provider, input.summaryModel.id, input.tokenCalibration),
|
|
4756
|
-
adapted: damageMedian >= 25,
|
|
4757
|
-
damageMedian
|
|
4758
|
-
};
|
|
4759
|
-
}
|
|
4760
|
-
function planManualPreflight(ctx, summaryModel, mode, tokenCalibration, config, damageMedian) {
|
|
4761
|
-
const prepared = preparePreflightProfile({ cwd: ctx.cwd, summaryModel, mode, tokenCalibration, config, damageMedian });
|
|
4762
|
-
const manager = ctx.sessionManager;
|
|
4763
|
-
const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
|
|
4764
|
-
const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
|
|
4765
|
-
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
4766
|
-
const contextWindowTokens = ctx.model?.contextWindow ?? 0;
|
|
4767
|
-
const contextPercent = contextWindowTokens > 0 ? totalTokens / contextWindowTokens * 100 : 0;
|
|
4768
|
-
const toolPercent = computeToolCharPercentage(branch);
|
|
4769
|
-
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
4770
|
-
const messageTokens = msgs.map((entry) => prepared.estimator.message(entry.message));
|
|
4771
|
-
const rawEstimatedMessageTokens = messageTokens.reduce((sum, tokens) => sum + tokens, 0);
|
|
4772
|
-
if (msgs.length < 3) {
|
|
4773
|
-
return { mode, plan: null, reason: "not-enough-messages", profileCfg: prepared.profileCfg, totalTokens, rawEstimatedMessageTokens, estimatorScale: 1, adapted: prepared.adapted, damageMedian: prepared.damageMedian, contextWindowTokens, contextPercent, toolPercent, overflowedContext };
|
|
4774
|
-
}
|
|
4775
|
-
const plan = planCompactionWindow({
|
|
4776
|
-
msgs,
|
|
4777
|
-
branch,
|
|
4778
|
-
messageTokens,
|
|
4779
|
-
totalTokens,
|
|
4780
|
-
modelContextWindow: ctx.model?.contextWindow,
|
|
4781
|
-
mode,
|
|
4782
|
-
profileCfg: prepared.profileCfg,
|
|
4783
|
-
force: true,
|
|
4784
|
-
overflowedContext
|
|
4785
|
-
});
|
|
4786
|
-
const normalizedMessages = plan.compactTokens + plan.retainedTokens;
|
|
4787
|
-
return {
|
|
4788
|
-
mode,
|
|
4789
|
-
plan,
|
|
4790
|
-
reason: plan.reason,
|
|
4791
|
-
profileCfg: prepared.profileCfg,
|
|
4792
|
-
totalTokens,
|
|
4793
|
-
rawEstimatedMessageTokens,
|
|
4794
|
-
estimatorScale: rawEstimatedMessageTokens > 0 ? normalizedMessages / rawEstimatedMessageTokens : 1,
|
|
4795
|
-
adapted: prepared.adapted,
|
|
4796
|
-
damageMedian: prepared.damageMedian,
|
|
4797
|
-
contextWindowTokens,
|
|
4798
|
-
contextPercent,
|
|
4799
|
-
toolPercent,
|
|
4800
|
-
overflowedContext
|
|
4801
|
-
};
|
|
4802
|
-
}
|
|
4803
|
-
|
|
4804
|
-
// src/ui/overlays.ts
|
|
4805
|
-
import path8 from "path";
|
|
4806
|
-
|
|
4807
|
-
// src/utils/state.ts
|
|
4808
|
-
import fs5 from "fs";
|
|
4809
|
-
|
|
4810
|
-
// src/domain/summary-schema.ts
|
|
4811
|
-
function classifyHeading(raw) {
|
|
4812
|
-
const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
|
|
4813
|
-
if (!text)
|
|
4814
|
-
return "unknown";
|
|
4815
|
-
if (text === "goal" || text === "goals" || text === "objective" || text === "objectives")
|
|
4816
|
-
return "goal";
|
|
4817
|
-
if (text.startsWith("constraint") || text.includes("preference"))
|
|
4818
|
-
return "constraints";
|
|
4819
|
-
if (text === "progress" || text === "status")
|
|
4820
|
-
return "progress";
|
|
4821
|
-
if (text.includes("key decision") || text === "decisions")
|
|
4822
|
-
return "decisions";
|
|
4823
|
-
if (text.includes("file") && text.includes("modif"))
|
|
4824
|
-
return "files-modified";
|
|
4825
|
-
if (text.includes("file") && (text.includes("read") || text.includes("viewed")))
|
|
4826
|
-
return "files-read";
|
|
4827
|
-
if (text.includes("next step") || text === "next actions")
|
|
4828
|
-
return "next-steps";
|
|
4829
|
-
if (text.includes("critical context") || text === "important context")
|
|
4830
|
-
return "critical-context";
|
|
4831
|
-
if (text === "topics" || text.includes("topics covered"))
|
|
4832
|
-
return "topics";
|
|
4833
|
-
if (text.includes("open loop") || text.includes("unresolved"))
|
|
4834
|
-
return "open-loops";
|
|
4835
|
-
if (text.includes("changes since") || text === "changes")
|
|
4836
|
-
return "changes";
|
|
4837
|
-
if (text.includes("verification"))
|
|
4838
|
-
return "verification-note";
|
|
4839
|
-
return "unknown";
|
|
4840
|
-
}
|
|
4841
|
-
function canonicalHeading(kind) {
|
|
4842
|
-
switch (kind) {
|
|
4843
|
-
case "goal":
|
|
4844
|
-
return SECTION_GOAL;
|
|
4845
|
-
case "constraints":
|
|
4846
|
-
return SECTION_CONSTRAINTS;
|
|
4847
|
-
case "progress":
|
|
4848
|
-
return SECTION_PROGRESS;
|
|
4849
|
-
case "decisions":
|
|
4850
|
-
return SECTION_DECISIONS;
|
|
4851
|
-
case "files-modified":
|
|
4852
|
-
return SECTION_FILES_MODIFIED;
|
|
4853
|
-
case "files-read":
|
|
4854
|
-
return SECTION_FILES_READ;
|
|
4855
|
-
case "next-steps":
|
|
4856
|
-
return SECTION_NEXT_STEPS;
|
|
4857
|
-
case "critical-context":
|
|
4858
|
-
return SECTION_CRITICAL_CONTEXT;
|
|
4859
|
-
case "topics":
|
|
4860
|
-
return SECTION_TOPICS;
|
|
4861
|
-
case "open-loops":
|
|
4862
|
-
return SECTION_OPEN_LOOPS;
|
|
4863
|
-
case "changes":
|
|
4864
|
-
return SECTION_CHANGES;
|
|
4865
|
-
case "verification-note":
|
|
4866
|
-
return "## Verification Note";
|
|
4867
|
-
case "unknown":
|
|
4868
|
-
default:
|
|
4869
|
-
return "## Section";
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
|
|
4873
|
-
// src/domain/summary-parse.ts
|
|
4874
|
-
var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
4875
|
-
function summaryEvidenceLine(value, maxLength) {
|
|
4876
|
-
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
|
|
4877
|
-
}
|
|
4878
|
-
function mergeBodies(first, second) {
|
|
4879
|
-
const seen = new Set;
|
|
4880
|
-
return [first, second].filter(Boolean).flatMap((body) => body.split(`
|
|
4881
|
-
`)).filter((line) => seen.has(line) ? false : (seen.add(line), true)).join(`
|
|
4882
|
-
`).trim();
|
|
4883
|
-
}
|
|
4884
|
-
function parseSummary(markdown) {
|
|
4885
|
-
const sections = [];
|
|
4886
|
-
const lines = markdown.split(`
|
|
4887
|
-
`);
|
|
4888
|
-
let currentHeading = "";
|
|
4889
|
-
let currentKind = "unknown";
|
|
4890
|
-
let bodyLines = [];
|
|
4891
|
-
let started = false;
|
|
4892
|
-
const flush = () => {
|
|
4893
|
-
if (!started)
|
|
4894
|
-
return;
|
|
4895
|
-
const body = bodyLines.join(`
|
|
4896
|
-
`).trim();
|
|
4897
|
-
const existing = currentKind === "unknown" ? undefined : sections.find((s) => s.kind === currentKind);
|
|
4898
|
-
if (existing)
|
|
4899
|
-
existing.body = mergeBodies(existing.body, body);
|
|
4900
|
-
else
|
|
4901
|
-
sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
|
|
4902
|
-
};
|
|
4903
|
-
for (const line of lines) {
|
|
4904
|
-
const m = line.match(HEADING_RE);
|
|
4905
|
-
if (m) {
|
|
4906
|
-
const kind = classifyHeading(m[2]);
|
|
4907
|
-
if (m[1].length <= 2 || kind !== "unknown") {
|
|
4908
|
-
flush();
|
|
4909
|
-
currentHeading = "## " + m[2].trim();
|
|
4910
|
-
currentKind = kind;
|
|
4911
|
-
bodyLines = [];
|
|
4912
|
-
started = true;
|
|
4913
|
-
continue;
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
if (started)
|
|
4917
|
-
bodyLines.push(line);
|
|
4918
|
-
}
|
|
4919
|
-
flush();
|
|
4920
|
-
return { sections };
|
|
4921
|
-
}
|
|
4922
|
-
function findSection(summary, kind) {
|
|
4923
|
-
const parsed = typeof summary === "string" ? parseSummary(summary) : summary;
|
|
4924
|
-
return parsed.sections.find((s) => s.kind === kind);
|
|
4925
|
-
}
|
|
4926
|
-
function renderSummary(summary, opts = {}) {
|
|
4927
|
-
return summary.sections.map((s) => {
|
|
4928
|
-
const heading = opts.canonicalHeadings && s.kind !== "unknown" ? canonicalHeading(s.kind) : s.heading;
|
|
4929
|
-
return heading + `
|
|
4930
|
-
` + s.body;
|
|
4931
|
-
}).join(`
|
|
4932
|
-
|
|
4933
|
-
`).replace(/\n{3,}/g, `
|
|
4934
|
-
|
|
4935
|
-
`).trim() + `
|
|
4936
|
-
`;
|
|
4937
|
-
}
|
|
4938
|
-
function upsertSection(summary, kind, body, placement) {
|
|
4939
|
-
const heading = canonicalHeading(kind);
|
|
4940
|
-
const existing = summary.sections.findIndex((s) => s.kind === kind);
|
|
4941
|
-
if (existing >= 0) {
|
|
4942
|
-
const sections = summary.sections.slice();
|
|
4943
|
-
sections[existing] = { kind, heading, body: body.trim() };
|
|
4944
|
-
return { sections };
|
|
4945
|
-
}
|
|
4946
|
-
const hint = placement == null ? {} : typeof placement === "string" ? { before: placement } : placement;
|
|
4947
|
-
const section = { kind, heading, body: body.trim() };
|
|
4948
|
-
if (hint.before) {
|
|
4949
|
-
const idx = summary.sections.findIndex((s) => s.kind === hint.before);
|
|
4950
|
-
if (idx >= 0) {
|
|
4951
|
-
const sections = summary.sections.slice();
|
|
4952
|
-
sections.splice(idx, 0, section);
|
|
4953
|
-
return { sections };
|
|
4954
|
-
}
|
|
4955
|
-
}
|
|
4956
|
-
if (hint.after) {
|
|
4957
|
-
let idx = -1;
|
|
4958
|
-
for (let i = summary.sections.length - 1;i >= 0; i--) {
|
|
4959
|
-
if (summary.sections[i].kind === hint.after) {
|
|
4960
|
-
idx = i;
|
|
4961
|
-
break;
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
if (idx >= 0) {
|
|
4965
|
-
const sections = summary.sections.slice();
|
|
4966
|
-
sections.splice(idx + 1, 0, section);
|
|
4967
|
-
return { sections };
|
|
4968
|
-
}
|
|
4969
|
-
}
|
|
4970
|
-
return { sections: [...summary.sections, section] };
|
|
4956
|
+
const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
|
|
4957
|
+
if (rc.flags.force && rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
|
|
4958
|
+
rc.notify("Manual compaction override at " + Math.round(contextPercent) + "% (" + totalTokens.toLocaleString() + "t): compacting about " + plan.compactTokens.toLocaleString() + "t while preserving " + plan.retainedTokens.toLocaleString() + "t of recent context. Early compaction is lossy; verification remains fail-closed.", "warning");
|
|
4959
|
+
}
|
|
4960
|
+
const out = rc;
|
|
4961
|
+
out.sessionId = resolveSessionId(rc.ctx);
|
|
4962
|
+
out.branch = branch;
|
|
4963
|
+
out.msgs = msgs;
|
|
4964
|
+
out.totalTokens = totalTokens;
|
|
4965
|
+
out.contextPercent = contextPercent;
|
|
4966
|
+
out.toolPercent = 0;
|
|
4967
|
+
out.keepFrom = plan.keepFrom;
|
|
4968
|
+
out.toCompact = msgs.slice(0, plan.keepFrom);
|
|
4969
|
+
out.firstKeptId = msgs[plan.keepFrom].id;
|
|
4970
|
+
out.compactTokens = plan.compactTokens;
|
|
4971
|
+
out.accTokens = plan.retainedTokens;
|
|
4972
|
+
out.compactionPlan = plan;
|
|
4973
|
+
return advance(out, "_windowed");
|
|
4971
4974
|
}
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
if (
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4975
|
+
|
|
4976
|
+
// src/app/preflight.ts
|
|
4977
|
+
function preflightDamageMedian(cwd, config) {
|
|
4978
|
+
if (!config.adaptiveDamageFeedback)
|
|
4979
|
+
return 0;
|
|
4980
|
+
const projectId = deriveProjectIdFromCwd(cwd);
|
|
4981
|
+
if (!projectId)
|
|
4982
|
+
return 0;
|
|
4983
|
+
const recent = readRecentDamageScores(projectId, 5).slice(-3).sort((a, b) => a - b);
|
|
4984
|
+
return recent.length ? recent[Math.floor(recent.length / 2)] : 0;
|
|
4985
|
+
}
|
|
4986
|
+
function preparePreflightProfile(input) {
|
|
4987
|
+
const config = input.config;
|
|
4988
|
+
const profile = MODE_POLICIES[input.mode].profile;
|
|
4989
|
+
let profileCfg = { ...PROFILES[profile], ...config.profiles?.[profile] ?? {} };
|
|
4990
|
+
const damageMedian = input.damageMedian ?? preflightDamageMedian(input.cwd, config);
|
|
4991
|
+
if (damageMedian >= 25) {
|
|
4992
|
+
profileCfg = {
|
|
4993
|
+
...profileCfg,
|
|
4994
|
+
keepRecentTokens: Math.round(profileCfg.keepRecentTokens * (damageMedian >= 50 ? 1.5 : 1.25)),
|
|
4995
|
+
summaryBudgetTokens: Math.round(profileCfg.summaryBudgetTokens * (damageMedian >= 50 ? 1.3 : 1.2))
|
|
4996
|
+
};
|
|
4983
4997
|
}
|
|
4984
|
-
return
|
|
4985
|
-
|
|
4998
|
+
return {
|
|
4999
|
+
profileCfg,
|
|
5000
|
+
estimator: makeTokenEstimator(input.summaryModel.provider, input.summaryModel.id, input.tokenCalibration),
|
|
5001
|
+
adapted: damageMedian >= 25,
|
|
5002
|
+
damageMedian
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
function prepareManualPreflightContext(ctx, summaryModel, tokenCalibration) {
|
|
5006
|
+
const branch = typeof ctx.sessionManager.buildContextEntries === "function" ? ctx.sessionManager.buildContextEntries() : ctx.sessionManager.getBranch();
|
|
5007
|
+
const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
|
|
5008
|
+
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
5009
|
+
const modelContextWindow = ctx.model?.contextWindow;
|
|
5010
|
+
const contextWindowTokens = modelContextWindow ?? 0;
|
|
5011
|
+
const contextPercent = contextWindowTokens > 0 ? totalTokens / contextWindowTokens * 100 : 0;
|
|
5012
|
+
const toolPercent = computeToolCharPercentage(branch);
|
|
5013
|
+
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
5014
|
+
const estimator = makeTokenEstimator(summaryModel.provider, summaryModel.id, tokenCalibration);
|
|
5015
|
+
const messageTokens = msgs.map((entry) => estimator.message(entry.message));
|
|
5016
|
+
return {
|
|
5017
|
+
branch,
|
|
5018
|
+
msgs,
|
|
5019
|
+
messageTokens,
|
|
5020
|
+
totalTokens,
|
|
5021
|
+
rawEstimatedMessageTokens: messageTokens.reduce((sum, tokens) => sum + tokens, 0),
|
|
5022
|
+
modelContextWindow,
|
|
5023
|
+
contextWindowTokens,
|
|
5024
|
+
contextPercent,
|
|
5025
|
+
toolPercent,
|
|
5026
|
+
overflowedContext
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
5029
|
+
function planManualPreflight(ctx, summaryModel, mode, tokenCalibration, config, damageMedian, shared = prepareManualPreflightContext(ctx, summaryModel, tokenCalibration)) {
|
|
5030
|
+
const prepared = preparePreflightProfile({ cwd: ctx.cwd, summaryModel, mode, tokenCalibration, config, damageMedian });
|
|
5031
|
+
const {
|
|
5032
|
+
branch,
|
|
5033
|
+
msgs,
|
|
5034
|
+
messageTokens,
|
|
5035
|
+
totalTokens,
|
|
5036
|
+
rawEstimatedMessageTokens,
|
|
5037
|
+
modelContextWindow,
|
|
5038
|
+
contextWindowTokens,
|
|
5039
|
+
contextPercent,
|
|
5040
|
+
toolPercent,
|
|
5041
|
+
overflowedContext
|
|
5042
|
+
} = shared;
|
|
5043
|
+
if (msgs.length < 3) {
|
|
5044
|
+
return { mode, plan: null, reason: "not-enough-messages", profileCfg: prepared.profileCfg, totalTokens, rawEstimatedMessageTokens, estimatorScale: 1, adapted: prepared.adapted, damageMedian: prepared.damageMedian, contextWindowTokens, contextPercent, toolPercent, overflowedContext };
|
|
5045
|
+
}
|
|
5046
|
+
const plan = planCompactionWindow({
|
|
5047
|
+
msgs,
|
|
5048
|
+
branch,
|
|
5049
|
+
messageTokens,
|
|
5050
|
+
totalTokens,
|
|
5051
|
+
modelContextWindow,
|
|
5052
|
+
mode,
|
|
5053
|
+
profileCfg: prepared.profileCfg,
|
|
5054
|
+
force: true,
|
|
5055
|
+
overflowedContext
|
|
5056
|
+
});
|
|
5057
|
+
const normalizedMessages = plan.compactTokens + plan.retainedTokens;
|
|
5058
|
+
return {
|
|
5059
|
+
mode,
|
|
5060
|
+
plan,
|
|
5061
|
+
reason: plan.reason,
|
|
5062
|
+
profileCfg: prepared.profileCfg,
|
|
5063
|
+
totalTokens,
|
|
5064
|
+
rawEstimatedMessageTokens,
|
|
5065
|
+
estimatorScale: rawEstimatedMessageTokens > 0 ? normalizedMessages / rawEstimatedMessageTokens : 1,
|
|
5066
|
+
adapted: prepared.adapted,
|
|
5067
|
+
damageMedian: prepared.damageMedian,
|
|
5068
|
+
contextWindowTokens,
|
|
5069
|
+
contextPercent,
|
|
5070
|
+
toolPercent,
|
|
5071
|
+
overflowedContext
|
|
5072
|
+
};
|
|
4986
5073
|
}
|
|
4987
5074
|
|
|
5075
|
+
// src/ui/overlays.ts
|
|
5076
|
+
import path8 from "path";
|
|
5077
|
+
|
|
4988
5078
|
// src/utils/state.ts
|
|
5079
|
+
import fs5 from "fs";
|
|
4989
5080
|
function getStatePath(projectId, state) {
|
|
4990
5081
|
return state?.scope?.sessionId ? scopedCompactionStateFile(projectId, state.scope.sessionId) : compactionStateFile(projectId);
|
|
4991
5082
|
}
|
|
@@ -4994,12 +5085,16 @@ function isLegacySearchOutput(text) {
|
|
|
4994
5085
|
return /^[^\s:][^:]*:\d+(?::\d+)?:/.test(firstLine);
|
|
4995
5086
|
}
|
|
4996
5087
|
function sanitizeCompactionStateEvidence(state) {
|
|
5088
|
+
const isNoise = (text) => isLegacySearchOutput(text) || isTransientToolDiagnostic(text.replace(/^Unresolved error:\s*/i, ""));
|
|
5089
|
+
const goal = state.goal && !isCompactionStatusText(state.goal) ? state.goal : null;
|
|
4997
5090
|
const constraints = state.constraints.filter((item) => !isDiagnosticConstraintText(item.text));
|
|
4998
|
-
const unresolvedErrors = state.unresolvedErrors.filter((item) => !
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5091
|
+
const unresolvedErrors = state.unresolvedErrors.filter((item) => !isNoise(item.message));
|
|
5092
|
+
const resolvedErrors = state.resolvedErrors.filter((item) => !isNoise(item.message));
|
|
5093
|
+
const openLoops = state.openLoops.filter((item) => !isNoise(item.summary));
|
|
5094
|
+
const criticalContext = state.criticalContext.filter((item) => !isNoise(item));
|
|
5095
|
+
if (goal === state.goal && constraints.length === state.constraints.length && unresolvedErrors.length === state.unresolvedErrors.length && resolvedErrors.length === state.resolvedErrors.length && openLoops.length === state.openLoops.length && criticalContext.length === state.criticalContext.length)
|
|
5001
5096
|
return state;
|
|
5002
|
-
return { ...state, constraints, unresolvedErrors, openLoops };
|
|
5097
|
+
return { ...state, goal, constraints, unresolvedErrors, resolvedErrors, openLoops, criticalContext };
|
|
5003
5098
|
}
|
|
5004
5099
|
function freshState(fp, data) {
|
|
5005
5100
|
if (!data)
|
|
@@ -5022,14 +5117,14 @@ function saveCompactionState(projectId, state) {
|
|
|
5022
5117
|
warn("saveCompactionState failed", e);
|
|
5023
5118
|
}
|
|
5024
5119
|
}
|
|
5025
|
-
function loadScopedCompactionState(scope,
|
|
5120
|
+
function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
5026
5121
|
const fp = scopedCompactionStateFile(scope.projectId, scope.sessionId);
|
|
5027
5122
|
const state = freshState(fp, readJsonSync(fp));
|
|
5028
5123
|
if (!state?.scope || state.scope.schemaVersion !== 2)
|
|
5029
5124
|
return null;
|
|
5030
5125
|
if (state.scope.projectId !== scope.projectId || state.scope.sessionId !== scope.sessionId)
|
|
5031
5126
|
return null;
|
|
5032
|
-
if (state.scope.branchHeadId &&
|
|
5127
|
+
if (state.scope.branchHeadId && branchEntryIds2.length > 0 && !branchEntryIds2.includes(state.scope.branchHeadId))
|
|
5033
5128
|
return null;
|
|
5034
5129
|
return state;
|
|
5035
5130
|
}
|
|
@@ -5133,26 +5228,34 @@ function mergeBy(current, previous, key, limit) {
|
|
|
5133
5228
|
return true;
|
|
5134
5229
|
}).slice(0, limit);
|
|
5135
5230
|
}
|
|
5231
|
+
var LOOP_PRIORITY = { critical: 0, high: 1, normal: 2, low: 3 };
|
|
5232
|
+
function mergeOpenLoops(current, previous) {
|
|
5233
|
+
return mergeBy(current, previous, (item) => normalizeFactKey(item.summary), Number.MAX_SAFE_INTEGER).map((item, order) => ({ item, order })).sort((a, b) => Number(a.item.status === "resolved") - Number(b.item.status === "resolved") || LOOP_PRIORITY[a.item.priority] - LOOP_PRIORITY[b.item.priority] || a.order - b.order).slice(0, 25).map(({ item }, index) => ({ ...item, id: ID_PREFIX.OPEN_LOOP + (index + 1) }));
|
|
5234
|
+
}
|
|
5136
5235
|
function mergeCompactionStates(previous, current) {
|
|
5137
|
-
if (!previous)
|
|
5138
|
-
|
|
5236
|
+
if (!previous) {
|
|
5237
|
+
const active = applyContinuityOverrides(current, current.factOverrides ?? []);
|
|
5238
|
+
return { ...active, openLoops: mergeOpenLoops(active.openLoops, []) };
|
|
5239
|
+
}
|
|
5139
5240
|
const factOverrides = mergeBy(current.factOverrides ?? [], previous.factOverrides ?? [], (item) => item.kind + ":" + item.summaryKey, 50);
|
|
5140
5241
|
const activeCurrent = applyContinuityOverrides(current, factOverrides);
|
|
5141
5242
|
const activePrevious = applyContinuityOverrides(previous, factOverrides);
|
|
5243
|
+
const currentPresent = new Set([...activeCurrent.modifiedFiles, ...activeCurrent.readFiles].map(normalizeFactKey));
|
|
5244
|
+
const currentDeleted = new Set(activeCurrent.deletedFiles.map(normalizeFactKey));
|
|
5142
5245
|
const resolvedKeys = new Set(activeCurrent.resolvedErrors.map((error2) => normalizeFactKey(error2.message)));
|
|
5143
5246
|
const decisions = mergeBy(activeCurrent.decisions, activePrevious.decisions, (item) => normalizeFactKey(item.summary), 30).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) }));
|
|
5144
5247
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
5145
5248
|
const unresolvedErrors = mergeBy(activeCurrent.unresolvedErrors, activePrevious.unresolvedErrors.filter((error2) => !resolvedKeys.has(normalizeFactKey(error2.message))), (item) => normalizeFactKey(item.message), 15).map((item, index) => ({ ...item, id: ID_PREFIX.ERROR + (index + 1) }));
|
|
5146
|
-
const openLoops =
|
|
5249
|
+
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops);
|
|
5147
5250
|
const oldGoal = activePrevious.goal && activeCurrent.goal && normalizeFactKey(activePrevious.goal) !== normalizeFactKey(activeCurrent.goal) ? ["Previous goal: " + activePrevious.goal] : [];
|
|
5148
5251
|
return applyContinuityOverrides({
|
|
5149
5252
|
...activeCurrent,
|
|
5150
5253
|
goal: activeCurrent.goal ?? activePrevious.goal,
|
|
5151
5254
|
decisions,
|
|
5152
5255
|
constraints,
|
|
5153
|
-
modifiedFiles: mergeBy(activeCurrent.modifiedFiles, activePrevious.modifiedFiles, normalizeFactKey, 100),
|
|
5154
|
-
readFiles: mergeBy(activeCurrent.readFiles, activePrevious.readFiles, normalizeFactKey, 100),
|
|
5155
|
-
deletedFiles: mergeBy(activeCurrent.deletedFiles, activePrevious.deletedFiles, normalizeFactKey, 50),
|
|
5256
|
+
modifiedFiles: mergeBy(activeCurrent.modifiedFiles, activePrevious.modifiedFiles.filter((file) => !currentDeleted.has(normalizeFactKey(file))), normalizeFactKey, 100),
|
|
5257
|
+
readFiles: mergeBy(activeCurrent.readFiles, activePrevious.readFiles.filter((file) => !currentDeleted.has(normalizeFactKey(file))), normalizeFactKey, 100),
|
|
5258
|
+
deletedFiles: mergeBy(activeCurrent.deletedFiles, activePrevious.deletedFiles.filter((file) => !currentPresent.has(normalizeFactKey(file))), normalizeFactKey, 50),
|
|
5156
5259
|
unresolvedErrors,
|
|
5157
5260
|
resolvedErrors: mergeBy(activeCurrent.resolvedErrors, activePrevious.resolvedErrors, (item) => normalizeFactKey(item.message), 20),
|
|
5158
5261
|
openLoops,
|
|
@@ -5206,28 +5309,37 @@ function injectOpenLoopsSection(summary, openLoops) {
|
|
|
5206
5309
|
return renderSummary(updated);
|
|
5207
5310
|
}
|
|
5208
5311
|
function computeDelta(prev, current) {
|
|
5209
|
-
const
|
|
5210
|
-
const
|
|
5211
|
-
const
|
|
5212
|
-
const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(
|
|
5213
|
-
const
|
|
5214
|
-
const
|
|
5215
|
-
const
|
|
5312
|
+
const overrides = current.factOverrides ?? [];
|
|
5313
|
+
const retired = (kind) => new Set(overrides.filter((item) => item.kind === kind && item.status !== "active").map((item) => item.summaryKey));
|
|
5314
|
+
const prevDecisionTexts = new Set(prev.decisions.map((d) => normalizeFactKey(d.summary)));
|
|
5315
|
+
const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(normalizeFactKey(d.summary))).map((d) => d.summary);
|
|
5316
|
+
const retiredDecisions = retired("decision");
|
|
5317
|
+
const removedDecisions = prev.decisions.filter((d) => retiredDecisions.has(normalizeFactKey(d.summary))).map((d) => d.summary);
|
|
5318
|
+
const prevLoopSummaries = new Map(prev.openLoops.filter((loop) => loop.status !== "resolved").map((l) => [normalizeFactKey(l.summary), l]));
|
|
5319
|
+
const currLoopKeys = new Set(current.openLoops.filter((loop) => loop.status !== "resolved").map((l) => normalizeFactKey(l.summary)));
|
|
5320
|
+
const resolvedLoopKeys = new Set([
|
|
5321
|
+
...current.openLoops.filter((loop) => loop.status === "resolved").map((loop) => normalizeFactKey(loop.summary)),
|
|
5322
|
+
...(current.loopOverrides ?? []).filter((item) => item.status === "resolved").map((item) => item.summaryKey),
|
|
5323
|
+
...retired("loop")
|
|
5324
|
+
]);
|
|
5216
5325
|
const resolvedLoops = [];
|
|
5217
5326
|
const persistentLoops = [];
|
|
5218
5327
|
for (const [k, loop] of prevLoopSummaries) {
|
|
5219
5328
|
if (currLoopKeys.has(k))
|
|
5220
5329
|
persistentLoops.push(loop.summary);
|
|
5221
|
-
else
|
|
5330
|
+
else if (resolvedLoopKeys.has(k))
|
|
5222
5331
|
resolvedLoops.push(loop.summary);
|
|
5223
5332
|
}
|
|
5224
|
-
const newLoops = current.openLoops.filter((loop) => loop.status !== "resolved").filter((l) => !prevLoopSummaries.has(
|
|
5333
|
+
const newLoops = current.openLoops.filter((loop) => loop.status !== "resolved").filter((l) => !prevLoopSummaries.has(normalizeFactKey(l.summary))).map((l) => l.summary);
|
|
5225
5334
|
const prevFiles = new Set(prev.modifiedFiles);
|
|
5226
5335
|
const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
|
|
5227
|
-
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) =>
|
|
5228
|
-
const
|
|
5229
|
-
|
|
5230
|
-
|
|
5336
|
+
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => normalizeFactKey(e.message)));
|
|
5337
|
+
const resolvedErrorKeys = new Set([
|
|
5338
|
+
...current.resolvedErrors.map((error2) => normalizeFactKey(error2.message)),
|
|
5339
|
+
...retired("error")
|
|
5340
|
+
]);
|
|
5341
|
+
const resolvedErrors = prev.unresolvedErrors.filter((e) => resolvedErrorKeys.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5342
|
+
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5231
5343
|
const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
|
|
5232
5344
|
return {
|
|
5233
5345
|
newDecisions,
|
|
@@ -5389,36 +5501,19 @@ async function selectModel(ctx, opts) {
|
|
|
5389
5501
|
return null;
|
|
5390
5502
|
return options[parseInt(result.slice(6), 10)] ?? null;
|
|
5391
5503
|
}
|
|
5392
|
-
var PRIMARY_MODES = ["
|
|
5504
|
+
var PRIMARY_MODES = ["fast", "balanced", "thorough"];
|
|
5393
5505
|
var MODE_LABELS = {
|
|
5394
|
-
thorough: "Thorough",
|
|
5395
|
-
balanced: "Balanced",
|
|
5396
5506
|
fast: "Fast",
|
|
5397
|
-
|
|
5507
|
+
balanced: "Balanced",
|
|
5508
|
+
thorough: "Thorough"
|
|
5398
5509
|
};
|
|
5399
5510
|
var MODE_COPY = {
|
|
5400
|
-
|
|
5401
|
-
balanced: "default
|
|
5402
|
-
|
|
5403
|
-
aggressive: "maximum recovery \xB7 10K base recent tail"
|
|
5511
|
+
fast: "quickest \xB7 compact 10K recent tail \xB7 3K summary",
|
|
5512
|
+
balanced: "default quality/speed \xB7 20K recent tail \xB7 6K summary",
|
|
5513
|
+
thorough: "deepest analysis \xB7 rich 30K recent tail \xB7 10K summary"
|
|
5404
5514
|
};
|
|
5405
5515
|
function explainPreflightReason(reason) {
|
|
5406
|
-
|
|
5407
|
-
case "viable":
|
|
5408
|
-
return "safe window and useful estimated saving";
|
|
5409
|
-
case "not-enough-messages":
|
|
5410
|
-
return "fewer than 3 active messages";
|
|
5411
|
-
case "no-eligible-prefix":
|
|
5412
|
-
return "no older prefix is available";
|
|
5413
|
-
case "unsafe-tool-boundary":
|
|
5414
|
-
return "no complete tool-call boundary is available";
|
|
5415
|
-
case "retention-target-exceeded":
|
|
5416
|
-
return "a complete tool pair exceeds the tail target";
|
|
5417
|
-
case "mode-target-not-met":
|
|
5418
|
-
return "the estimated result misses this preset's target";
|
|
5419
|
-
case "insufficient-projected-saving":
|
|
5420
|
-
return "estimated saving is below 10%";
|
|
5421
|
-
}
|
|
5516
|
+
return reason === "not-enough-messages" ? "fewer than 3 active messages" : compactionPlanReasonText(reason);
|
|
5422
5517
|
}
|
|
5423
5518
|
function recommendationEvidence(preflight) {
|
|
5424
5519
|
const yieldPercent = Math.round((preflight.plan?.projectedYield ?? 0) * 100);
|
|
@@ -5438,7 +5533,7 @@ function recommendPreflight(plans) {
|
|
|
5438
5533
|
if (balanced?.plan?.viable) {
|
|
5439
5534
|
return { mode: "balanced", reason: "normal pressure favors the default balance; " + recommendationEvidence(balanced) };
|
|
5440
5535
|
}
|
|
5441
|
-
const fallback = ["
|
|
5536
|
+
const fallback = ["fast", "thorough"].find((mode) => plans.get(mode)?.plan?.viable);
|
|
5442
5537
|
if (fallback) {
|
|
5443
5538
|
const chosen = plans.get(fallback);
|
|
5444
5539
|
return {
|
|
@@ -5451,12 +5546,15 @@ function recommendPreflight(plans) {
|
|
|
5451
5546
|
function tokenCount(value) {
|
|
5452
5547
|
return Math.round(value).toLocaleString() + "t";
|
|
5453
5548
|
}
|
|
5549
|
+
function compactTokenCount(value) {
|
|
5550
|
+
if (Math.abs(value) < 1000)
|
|
5551
|
+
return Math.round(value) + "t";
|
|
5552
|
+
const scaled = value / 1000;
|
|
5553
|
+
return scaled.toFixed(scaled >= 10 ? 1 : 2).replace(/\.0+$|(\.\d*[1-9])0+$/, "$1") + "K";
|
|
5554
|
+
}
|
|
5454
5555
|
function percent(value) {
|
|
5455
5556
|
return Math.round(value).toLocaleString() + "%";
|
|
5456
5557
|
}
|
|
5457
|
-
function windowPercent(tokens, window) {
|
|
5458
|
-
return window > 0 ? percent(tokens / window * 100) : "unknown %";
|
|
5459
|
-
}
|
|
5460
5558
|
var SOFT_BOUNDARY_COPY = {
|
|
5461
5559
|
"recent-user-turn": "older user turn",
|
|
5462
5560
|
anchor: "latest checkpoint",
|
|
@@ -5468,26 +5566,23 @@ function formatPreflightSummary(preflight, modelLabel, details = false) {
|
|
|
5468
5566
|
const plan = preflight.plan;
|
|
5469
5567
|
if (!plan) {
|
|
5470
5568
|
const lines2 = [
|
|
5471
|
-
"
|
|
5472
|
-
"
|
|
5473
|
-
"Hard safeguards: complete tool-call/result pairs and zero-gap verification before apply."
|
|
5569
|
+
"Plan unavailable \xB7 " + explainPreflightReason(preflight.reason),
|
|
5570
|
+
"\u2713 Complete tool pairs \xB7 \u2713 zero-gap verification before apply"
|
|
5474
5571
|
];
|
|
5475
5572
|
if (details)
|
|
5476
|
-
lines2.push("Estimator
|
|
5573
|
+
lines2.push("Estimator messages ~" + tokenCount(preflight.rawEstimatedMessageTokens) + " \xB7 normalization unavailable", "Route " + modelLabel + " \xB7 viability " + preflight.reason);
|
|
5477
5574
|
return lines2;
|
|
5478
5575
|
}
|
|
5479
|
-
const
|
|
5576
|
+
const stateReserve = Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO);
|
|
5480
5577
|
const lines = [
|
|
5481
|
-
"
|
|
5482
|
-
"
|
|
5483
|
-
"
|
|
5484
|
-
soft,
|
|
5485
|
-
"Hard safeguards: complete tool-call/result pairs and zero-gap verification before apply; " + (plan.hardBoundaryAdjusted ? "boundary adjusted to keep a pair intact" : "no hard-boundary adjustment needed")
|
|
5578
|
+
"Plan " + compactTokenCount(preflight.totalTokens) + " \u2192 ~" + compactTokenCount(plan.projectedAfterTokens) + " \xB7 ~" + compactTokenCount(plan.projectedSavedTokens) + " saved (" + percent(plan.projectedYield * 100) + ")",
|
|
5579
|
+
"Keep ~" + compactTokenCount(plan.retainedTokens) + " recent \xB7 summary up to " + compactTokenCount(plan.summaryBudgetTokens) + " + ~" + compactTokenCount(stateReserve) + " verified-state reserve",
|
|
5580
|
+
"\u2713 Complete tool pairs \xB7 \u2713 zero-gap verification before apply"
|
|
5486
5581
|
];
|
|
5487
5582
|
if (!plan.viable)
|
|
5488
|
-
lines.
|
|
5583
|
+
lines.unshift("Unavailable \xB7 " + explainPreflightReason(plan.reason));
|
|
5489
5584
|
if (details)
|
|
5490
|
-
lines.push("
|
|
5585
|
+
lines.push("Target \u2264" + tokenCount(plan.targetAfterTokens) + " \xB7 tail \u2264" + tokenCount(plan.retentionTargetTokens) + " \xB7 fixed ~" + tokenCount(plan.fixedContextTokens), "Estimator ~" + tokenCount(preflight.rawEstimatedMessageTokens) + " messages \xB7 normalized \xD7" + preflight.estimatorScale.toFixed(2), "Boundary " + (plan.hardBoundaryAdjusted ? "tool pair kept intact" : "no hard adjustment") + " \xB7 soft summarized: " + (plan.relaxedSoftBoundaries.map((kind) => SOFT_BOUNDARY_COPY[kind] ?? kind).join(", ") || "none"), "Route " + modelLabel + (preflight.adapted ? " \xB7 damage feedback " + preflight.damageMedian + "/100" : ""));
|
|
5491
5586
|
return lines;
|
|
5492
5587
|
}
|
|
5493
5588
|
var PROGRESS_KEY = "smart-compact-progress";
|
|
@@ -5496,12 +5591,25 @@ function showProgressOverlay(ctx, state) {
|
|
|
5496
5591
|
if (ctx.hasUI === false)
|
|
5497
5592
|
return;
|
|
5498
5593
|
const name = PROGRESS_PHASES[state.phase - 1] ?? state.phaseName;
|
|
5499
|
-
const story = PROGRESS_PHASES.map((phase, index) => index === 1 && state.phase > 2 && !state.explorationRounds ? "\u2013 Explore" : index < state.phase - 1 ? "\u2713 " + phase : index === state.phase - 1 ? "\u25CF " + phase : "\u25CB " + phase).join(" ");
|
|
5500
|
-
const detail = state.detail ? name + " \xB7 " + state.detail : name;
|
|
5501
5594
|
try {
|
|
5502
|
-
ctx.ui.setStatus?.(PROGRESS_KEY, "Smart Compact \xB7 " +
|
|
5595
|
+
ctx.ui.setStatus?.(PROGRESS_KEY, "Smart Compact " + state.phase + "/5 \xB7 " + name);
|
|
5503
5596
|
ctx.ui.setWidget?.(PROGRESS_KEY, (_tui, theme) => ({
|
|
5504
|
-
render: (width) =>
|
|
5597
|
+
render: (width) => {
|
|
5598
|
+
const story = PROGRESS_PHASES.map((phase, index) => {
|
|
5599
|
+
if (index === 1 && state.phase > 2 && !state.explorationRounds)
|
|
5600
|
+
return theme.fg("dim", "\u2013 Explore");
|
|
5601
|
+
if (index < state.phase - 1)
|
|
5602
|
+
return theme.fg("success", "\u2713 " + phase);
|
|
5603
|
+
if (index === state.phase - 1)
|
|
5604
|
+
return theme.fg("accent", theme.bold("\u25CF " + phase));
|
|
5605
|
+
return theme.fg("dim", "\u25CB " + phase);
|
|
5606
|
+
}).join(theme.fg("dim", " "));
|
|
5607
|
+
const safety = state.phase < 5 ? " \xB7 conversation unchanged" : "";
|
|
5608
|
+
return [
|
|
5609
|
+
truncateToWidth(story, width),
|
|
5610
|
+
truncateToWidth(theme.fg("muted", "\u21B3 " + state.detail + safety), width)
|
|
5611
|
+
];
|
|
5612
|
+
},
|
|
5505
5613
|
invalidate: () => {}
|
|
5506
5614
|
}), { placement: "belowEditor" });
|
|
5507
5615
|
} catch {}
|
|
@@ -5517,8 +5625,11 @@ function notifyAppliedCompaction(ctx, details, concise) {
|
|
|
5517
5625
|
const after = details.estimatedAfterTokens ?? Math.max(0, before - details.tokensSaved);
|
|
5518
5626
|
const saving = Math.round((details.estimatedYield ?? (before ? details.tokensSaved / before : 0)) * 100);
|
|
5519
5627
|
const quality = details.qualityScore ?? 0;
|
|
5628
|
+
const initial = details.provenance?.initialScore ?? quality;
|
|
5629
|
+
const repaired = details.provenance && (details.provenance.deterministicPatched.length > 0 || details.provenance.llmPatched || details.provenance.qualityFloorUsed);
|
|
5630
|
+
const verification = "verified " + quality + "/100 coverage" + (repaired ? " (source " + initial + "/100" + (details.provenance?.qualityFloorUsed ? ", safety fallback" : "") + ")" : "") + " \xB7 0 gaps";
|
|
5520
5631
|
const planned = details.plannedAfterTokens ?? after;
|
|
5521
|
-
ctx.ui.notify(concise ? "Smart compact applied \u2713 \xB7 " + before.toLocaleString() + "t \u2192 ~" + after.toLocaleString() + "t estimate (plan ~" + planned.toLocaleString() + "t) \xB7 " + saving + "% saved \xB7
|
|
5632
|
+
ctx.ui.notify(concise ? "Smart compact applied \u2713 \xB7 " + before.toLocaleString() + "t \u2192 ~" + after.toLocaleString() + "t estimate (plan ~" + planned.toLocaleString() + "t) \xB7 " + saving + "% saved \xB7 " + verification : "Smart compact applied \u2713 \u2014 " + before.toLocaleString() + "t \u2192 planned ~" + planned.toLocaleString() + "t / ~" + after.toLocaleString() + "t applied estimate \xB7 saved " + saving + "% \xB7 " + verification, "info");
|
|
5522
5633
|
}
|
|
5523
5634
|
async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
5524
5635
|
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
@@ -5540,10 +5651,13 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
5540
5651
|
c.addChild(new Text(theme.fg("dim", " Routes: Explore " + details.providerRoutes.explore + " \u2022 Synthesize " + details.providerRoutes.synthesize + " \u2022 Verify " + details.providerRoutes.verify), 0, 0));
|
|
5541
5652
|
}
|
|
5542
5653
|
const scoreColor = details.qualityScore >= 80 ? "success" : details.qualityScore >= 50 ? "warning" : "error";
|
|
5543
|
-
c.addChild(new Text(theme.fg("text", "
|
|
5654
|
+
c.addChild(new Text(theme.fg("text", " Verification coverage: ") + theme.fg(scoreColor, details.qualityScore + "/100"), 0, 0));
|
|
5544
5655
|
if (details.provenance) {
|
|
5545
5656
|
const provenance = details.provenance;
|
|
5546
|
-
c.addChild(new Text(theme.fg("dim", " Provenance:
|
|
5657
|
+
c.addChild(new Text(theme.fg("dim", " Provenance: source " + provenance.initialScore + " \u2192 deterministic " + provenance.deterministicPatched.length + (provenance.llmPatched ? " \u2192 LLM patch" : "") + " \u2192 verified " + provenance.finalScore + " (" + provenance.remainingGaps.length + " remaining)"), 0, 0));
|
|
5658
|
+
if (provenance.qualityFloorUsed) {
|
|
5659
|
+
c.addChild(new Text(theme.fg("warning", " Safety fallback used \xB7 verified coverage is not raw synthesis quality"), 0, 0));
|
|
5660
|
+
}
|
|
5547
5661
|
}
|
|
5548
5662
|
if ((details.redactions ?? 0) > 0) {
|
|
5549
5663
|
c.addChild(new Text(theme.fg("warning", " Security: " + details.redactions + " sensitive value(s) redacted"), 0, 0));
|
|
@@ -5639,7 +5753,7 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
5639
5753
|
}, { overlay: true, overlayOptions: { width: "70%", anchor: "center", maxHeight: "80%" } });
|
|
5640
5754
|
if (!opts.approval)
|
|
5641
5755
|
return "closed";
|
|
5642
|
-
const approved = await ctx.ui.confirm("Apply Smart Compact?", "
|
|
5756
|
+
const approved = await ctx.ui.confirm("Apply Smart Compact?", "Verification coverage " + details.qualityScore + "/100 \xB7 source " + (details.provenance?.initialScore ?? details.qualityScore) + "/100 \xB7 " + details.gaps.length + " remaining gap(s) \xB7 " + details.tokensSaved.toLocaleString() + ` estimated tokens saved.
|
|
5643
5757
|
|
|
5644
5758
|
Cancel keeps the current conversation unchanged.`);
|
|
5645
5759
|
return approved ? "apply" : "cancel";
|
|
@@ -5779,33 +5893,65 @@ async function showCompactUI(ctx, opts) {
|
|
|
5779
5893
|
const calibration = createProductionServices().tokenCalibration;
|
|
5780
5894
|
const damageMedian = preflightDamageMedian(ctx.cwd, opts.config);
|
|
5781
5895
|
while (true) {
|
|
5782
|
-
const
|
|
5896
|
+
const shared = prepareManualPreflightContext(ctx, selectedModel.model, calibration);
|
|
5897
|
+
const plans = new Map(PRIMARY_MODES.map((mode) => [
|
|
5898
|
+
mode,
|
|
5899
|
+
planManualPreflight(ctx, selectedModel.model, mode, calibration, opts.config, damageMedian, shared)
|
|
5900
|
+
]));
|
|
5783
5901
|
const recommended = recommendPreflight(plans);
|
|
5784
5902
|
const action = await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
5785
5903
|
let selected = Math.max(0, PRIMARY_MODES.indexOf(recommended.mode));
|
|
5786
5904
|
let details = false;
|
|
5787
5905
|
return {
|
|
5788
5906
|
render: (width) => {
|
|
5789
|
-
const
|
|
5790
|
-
const
|
|
5907
|
+
const inner = Math.max(1, width - 2);
|
|
5908
|
+
const border = (text) => theme.fg("borderMuted", text);
|
|
5909
|
+
const fit = (text, max = inner) => truncateToWidth(text, Math.max(0, max), "");
|
|
5910
|
+
const fill = (text) => {
|
|
5911
|
+
const clipped = fit(text);
|
|
5912
|
+
return clipped + " ".repeat(Math.max(0, inner - visibleWidth(clipped)));
|
|
5913
|
+
};
|
|
5914
|
+
const cell = (text = "") => border("\u2502") + fill(text) + border("\u2502");
|
|
5915
|
+
const divider = border("\u251C" + "\u2500".repeat(inner) + "\u2524");
|
|
5916
|
+
const title = fit(" Smart Compact ", Math.max(0, inner - 1));
|
|
5917
|
+
const top = border("\u256D\u2500") + theme.fg("accent", theme.bold(title)) + border("\u2500".repeat(Math.max(0, inner - 1 - visibleWidth(title))) + "\u256E");
|
|
5918
|
+
const bottom = border("\u2570" + "\u2500".repeat(inner) + "\u256F");
|
|
5919
|
+
const selectedMode = PRIMARY_MODES[selected];
|
|
5920
|
+
const current = plans.get(selectedMode);
|
|
5921
|
+
const contextWindow = current.contextWindowTokens;
|
|
5922
|
+
const contextPct = Math.round(current.contextPercent);
|
|
5923
|
+
const barLength = width >= 72 ? 14 : 8;
|
|
5924
|
+
const barFilled = Math.min(barLength, Math.round(Math.min(100, contextPct) / 100 * barLength));
|
|
5925
|
+
const contextBar = theme.fg(contextPct >= 90 ? "error" : contextPct >= 70 ? "warning" : "success", "\u2588".repeat(barFilled)) + theme.fg("dim", "\u2591".repeat(barLength - barFilled));
|
|
5926
|
+
const modelPrefix = " Summary model ";
|
|
5927
|
+
const modelAction = theme.fg("accent", " [M] Change");
|
|
5928
|
+
const modelWidth = Math.max(1, inner - visibleWidth(modelPrefix) - visibleWidth(modelAction));
|
|
5791
5929
|
const lines = [
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
out("\u2500".repeat(Math.max(0, width)), "borderMuted")
|
|
5930
|
+
top,
|
|
5931
|
+
cell(" Context " + compactTokenCount(opts.contextTokens) + " / " + compactTokenCount(contextWindow) + " " + contextBar + " " + contextPct + "%"),
|
|
5932
|
+
cell(theme.fg("dim", modelPrefix) + fit(selectedModel.label, modelWidth) + modelAction),
|
|
5933
|
+
divider
|
|
5797
5934
|
];
|
|
5798
5935
|
for (let index = 0;index < PRIMARY_MODES.length; index++) {
|
|
5799
5936
|
const mode = PRIMARY_MODES[index];
|
|
5800
5937
|
const preview = plans.get(mode);
|
|
5801
|
-
const
|
|
5802
|
-
|
|
5803
|
-
|
|
5938
|
+
const plan = preview.plan;
|
|
5939
|
+
const viable = plan?.viable ?? false;
|
|
5940
|
+
const marker = index === selected ? "\u203A " : " ";
|
|
5941
|
+
const recommendedMark = mode === recommended.mode ? " recommended" : " ";
|
|
5942
|
+
const trait = mode === "fast" ? "quickest" : mode === "balanced" ? "default" : "deepest";
|
|
5943
|
+
const stats2 = (viable && plan ? "~" + compactTokenCount(plan.projectedAfterTokens) + " after \xB7 " + percent(plan.projectedYield * 100) + " saved" : "unavailable \xB7 " + explainPreflightReason(preview.reason)) + " \xB7 " + trait;
|
|
5944
|
+
const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " + stats2;
|
|
5945
|
+
lines.push(cell(index === selected ? theme.fg("accent", theme.bold(line)) : theme.fg(viable ? mode === recommended.mode ? "success" : "text" : "muted", line)));
|
|
5804
5946
|
}
|
|
5805
|
-
lines.push(
|
|
5806
|
-
const current
|
|
5807
|
-
|
|
5808
|
-
|
|
5947
|
+
lines.push(divider);
|
|
5948
|
+
for (const line of formatPreflightSummary(current, selectedModel.value, details)) {
|
|
5949
|
+
const color = line.startsWith("Unavailable") || line.startsWith("Plan unavailable") ? "warning" : line.startsWith("\u2713") ? "success" : "text";
|
|
5950
|
+
lines.push(cell(" " + theme.fg(color, line)));
|
|
5951
|
+
}
|
|
5952
|
+
if (details)
|
|
5953
|
+
lines.push(cell(" " + theme.fg("dim", MODE_LABELS[selectedMode] + " \xB7 " + MODE_COPY[selectedMode])));
|
|
5954
|
+
lines.push(divider, cell(theme.fg("dim", " \u2191\u2193 choose \xB7 Enter run \xB7 D details \xB7 M model \xB7 Esc cancel")), bottom);
|
|
5809
5955
|
return lines;
|
|
5810
5956
|
},
|
|
5811
5957
|
invalidate: () => {},
|
|
@@ -5815,9 +5961,9 @@ async function showCompactUI(ctx, opts) {
|
|
|
5815
5961
|
return;
|
|
5816
5962
|
}
|
|
5817
5963
|
if (keybindings.matches(data, "tui.select.up"))
|
|
5818
|
-
selected =
|
|
5964
|
+
selected = (selected + PRIMARY_MODES.length - 1) % PRIMARY_MODES.length;
|
|
5819
5965
|
else if (keybindings.matches(data, "tui.select.down"))
|
|
5820
|
-
selected =
|
|
5966
|
+
selected = (selected + 1) % PRIMARY_MODES.length;
|
|
5821
5967
|
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
5822
5968
|
const mode = PRIMARY_MODES[selected];
|
|
5823
5969
|
if (plans.get(mode)?.plan?.viable) {
|
|
@@ -5833,7 +5979,7 @@ async function showCompactUI(ctx, opts) {
|
|
|
5833
5979
|
tui.requestRender();
|
|
5834
5980
|
}
|
|
5835
5981
|
};
|
|
5836
|
-
}, { overlay: true, overlayOptions: { width: "
|
|
5982
|
+
}, { overlay: true, overlayOptions: { width: "68%", minWidth: 52, anchor: "center", maxHeight: "85%" } });
|
|
5837
5983
|
if (!action)
|
|
5838
5984
|
return null;
|
|
5839
5985
|
if (action === "model") {
|
|
@@ -6058,29 +6204,6 @@ function asSerializableMessages(msgs) {
|
|
|
6058
6204
|
import * as fs6 from "fs";
|
|
6059
6205
|
import * as path9 from "path";
|
|
6060
6206
|
import { StringDecoder } from "string_decoder";
|
|
6061
|
-
|
|
6062
|
-
// src/utils/lru.ts
|
|
6063
|
-
function lruGet(m, key) {
|
|
6064
|
-
if (!m.has(key))
|
|
6065
|
-
return;
|
|
6066
|
-
const v = m.get(key);
|
|
6067
|
-
m.delete(key);
|
|
6068
|
-
m.set(key, v);
|
|
6069
|
-
return v;
|
|
6070
|
-
}
|
|
6071
|
-
function lruSet(m, key, value, max) {
|
|
6072
|
-
if (m.has(key))
|
|
6073
|
-
m.delete(key);
|
|
6074
|
-
m.set(key, value);
|
|
6075
|
-
while (m.size > max) {
|
|
6076
|
-
const oldest = m.keys().next().value;
|
|
6077
|
-
if (oldest === undefined)
|
|
6078
|
-
break;
|
|
6079
|
-
m.delete(oldest);
|
|
6080
|
-
}
|
|
6081
|
-
}
|
|
6082
|
-
|
|
6083
|
-
// src/utils/session-log.ts
|
|
6084
6207
|
function getSessionsDir() {
|
|
6085
6208
|
return sessionsDir();
|
|
6086
6209
|
}
|
|
@@ -6412,7 +6535,7 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
6412
6535
|
if (block.name === "multi_tool_use.parallel" && Array.isArray(block.arguments?.tool_uses)) {
|
|
6413
6536
|
const tools = block.arguments.tool_uses;
|
|
6414
6537
|
const retained = tools.filter((tool, toolIndex) => {
|
|
6415
|
-
const id =
|
|
6538
|
+
const id = nestedToolCallId(block.id, idx, toolIndex, tool.id);
|
|
6416
6539
|
return !removedToolCallIds.has(id);
|
|
6417
6540
|
});
|
|
6418
6541
|
if (retained.length !== tools.length)
|
|
@@ -6467,7 +6590,8 @@ import { serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
|
6467
6590
|
function extractWithCache(rc) {
|
|
6468
6591
|
const extractStepStart = Date.now();
|
|
6469
6592
|
const currentEntryIds = rc.toCompact.map((e) => e.id);
|
|
6470
|
-
const
|
|
6593
|
+
const selectedMessages = rc.llmMessages;
|
|
6594
|
+
const pruning = pruneRedundant(selectedMessages);
|
|
6471
6595
|
const currentKeptEntryIds = pruning.keptIndices.map((i) => currentEntryIds[i]).filter((id) => typeof id === "string");
|
|
6472
6596
|
if (pruning.prunedCount > 0) {
|
|
6473
6597
|
rc.notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
@@ -6478,7 +6602,12 @@ function extractWithCache(rc) {
|
|
|
6478
6602
|
const extractionStart = pruneEnd;
|
|
6479
6603
|
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
6480
6604
|
const convTokens = rc.estimator.text(convText);
|
|
6481
|
-
|
|
6605
|
+
let backupPath = null;
|
|
6606
|
+
if (rc.config.backupEnabled) {
|
|
6607
|
+
const unchanged = pruning.messages.length === selectedMessages.length && pruning.messages.every((message, index) => message === selectedMessages[index]);
|
|
6608
|
+
const backupText = unchanged ? convText : serializeConversation(asSerializableMessages(selectedMessages));
|
|
6609
|
+
backupPath = backupConversation(rc.services.scrubber.scrubText(backupText).value, rc.sessionId);
|
|
6610
|
+
}
|
|
6482
6611
|
const prevContext = getPreviousCompactionContext(rc.branch);
|
|
6483
6612
|
const cachedExt = loadCachedExtraction(rc.sessionId);
|
|
6484
6613
|
let extraction;
|
|
@@ -6528,18 +6657,18 @@ function extractWithCache(rc) {
|
|
|
6528
6657
|
}
|
|
6529
6658
|
const projectCtx = buildProjectContext(fingerprint);
|
|
6530
6659
|
const manager = rc.ctx.sessionManager;
|
|
6531
|
-
const fullBranch = manager?.getBranch ?
|
|
6532
|
-
const
|
|
6660
|
+
const fullBranch = manager?.getBranch ? manager.getBranch() : rc.branch;
|
|
6661
|
+
const ancestryIds = branchEntryIds(fullBranch);
|
|
6533
6662
|
const continuityScope = {
|
|
6534
6663
|
schemaVersion: 2,
|
|
6535
6664
|
projectId,
|
|
6536
6665
|
sessionId: rc.sessionId,
|
|
6537
|
-
...
|
|
6538
|
-
branchHeadId:
|
|
6539
|
-
branchAncestryIds:
|
|
6666
|
+
...ancestryIds.length ? {
|
|
6667
|
+
branchHeadId: ancestryIds[ancestryIds.length - 1],
|
|
6668
|
+
branchAncestryIds: ancestryIds
|
|
6540
6669
|
} : {}
|
|
6541
6670
|
};
|
|
6542
|
-
const previousState = loadScopedCompactionState(continuityScope,
|
|
6671
|
+
const previousState = loadScopedCompactionState(continuityScope, ancestryIds);
|
|
6543
6672
|
const continuity = previousState ? renderContinuityCapsule(previousState) : "";
|
|
6544
6673
|
const out = rc;
|
|
6545
6674
|
out.pruning = pruning;
|
|
@@ -6855,7 +6984,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
6855
6984
|
tools: EXPLORATION_TOOLS
|
|
6856
6985
|
}, { apiKey: auth.apiKey, headers: auth.headers, signal, maxTokens: Math.min(4096, model.maxTokens || 4096) }, svc);
|
|
6857
6986
|
} catch (err) {
|
|
6858
|
-
|
|
6987
|
+
debugError("Explore loop stopped", err);
|
|
6859
6988
|
break;
|
|
6860
6989
|
}
|
|
6861
6990
|
const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
|
|
@@ -6976,6 +7105,7 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
6976
7105
|
return fallbackExplorationReport(llmMessages);
|
|
6977
7106
|
}
|
|
6978
7107
|
}
|
|
7108
|
+
|
|
6979
7109
|
// src/infra/synthesis-cache.ts
|
|
6980
7110
|
import { createHash as createHash2 } from "crypto";
|
|
6981
7111
|
var cache = new Map;
|
|
@@ -7004,7 +7134,7 @@ function synthesisCacheKey(rc) {
|
|
|
7004
7134
|
codexCallMs: rc.config.codexMaxCallMs,
|
|
7005
7135
|
latencyMs: rc.config.maxLatencyMs
|
|
7006
7136
|
},
|
|
7007
|
-
focus: rc.
|
|
7137
|
+
focus: rc.focus?.trim() || undefined,
|
|
7008
7138
|
zeroCall: rc.config.zeroCallEnabled !== false,
|
|
7009
7139
|
note: rc.userNote
|
|
7010
7140
|
});
|
|
@@ -7422,17 +7552,10 @@ async function summarizeConversation(rc) {
|
|
|
7422
7552
|
if (rc.requestedMode === "auto") {
|
|
7423
7553
|
const refined = resolveMode("auto", rc.contextPercent, extraction, continuityRisk(rc.previousState) + (rc.adapted ? 12 : 0));
|
|
7424
7554
|
if (refined !== rc.mode) {
|
|
7425
|
-
const oldBase = { ...PROFILES[rc.profile], ...rc.config.profiles?.[rc.profile] ?? {} };
|
|
7426
|
-
const keepScale = rc.profileCfg.keepRecentTokens / oldBase.keepRecentTokens;
|
|
7427
|
-
const summaryScale = rc.profileCfg.summaryBudgetTokens / oldBase.summaryBudgetTokens;
|
|
7428
7555
|
rc.mode = refined;
|
|
7429
|
-
rc.profile = MODE_POLICIES[refined].profile;
|
|
7430
|
-
rc.profileCfg = { ...PROFILES[rc.profile], ...rc.config.profiles?.[rc.profile] ?? {} };
|
|
7431
|
-
rc.profileCfg.keepRecentTokens = Math.round(rc.profileCfg.keepRecentTokens * keepScale);
|
|
7432
|
-
rc.profileCfg.summaryBudgetTokens = Math.round(rc.profileCfg.summaryBudgetTokens * summaryScale);
|
|
7433
7556
|
const policy2 = MODE_POLICIES[refined];
|
|
7434
7557
|
rc.services.budget.setLimits(rc.maxLlmCalls ?? effectiveBudget(rc.config.maxLlmCalls, policy2.maxLlmCalls), rc.maxLlmInputTokens ?? effectiveBudget(rc.config.maxLlmInputTokens, policy2.maxInputTokens), policy2.maxOutputTokens);
|
|
7435
|
-
rc.notify("Auto
|
|
7558
|
+
rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
|
|
7436
7559
|
}
|
|
7437
7560
|
}
|
|
7438
7561
|
const pc = rc.profileCfg;
|
|
@@ -7441,7 +7564,13 @@ async function summarizeConversation(rc) {
|
|
|
7441
7564
|
const cached = getCachedSynthesis(cacheKey);
|
|
7442
7565
|
if (cached) {
|
|
7443
7566
|
rc.notify("Synthesis cache hit \u2014 no LLM calls", "info");
|
|
7444
|
-
|
|
7567
|
+
if (!rc.flags.autoTriggered)
|
|
7568
|
+
showProgressOverlay(rc.ctx, {
|
|
7569
|
+
phase: 3,
|
|
7570
|
+
phaseName: "Synthesize",
|
|
7571
|
+
detail: "Reusing the cached continuation summary \xB7 no LLM call"
|
|
7572
|
+
});
|
|
7573
|
+
const hit = advance(rc, "_synthesized");
|
|
7445
7574
|
hit.finalSummary = cached.finalSummary;
|
|
7446
7575
|
hit.method = cached.method;
|
|
7447
7576
|
hit.methodForMetrics = cached.method + "-cache";
|
|
@@ -7451,13 +7580,19 @@ async function summarizeConversation(rc) {
|
|
|
7451
7580
|
hit.explorationRounds = cached.explorationRounds;
|
|
7452
7581
|
hit.chunkCount = cached.chunkCount;
|
|
7453
7582
|
markMeasuredPhase(hit, "synthesize", synthPhaseStart);
|
|
7454
|
-
return
|
|
7583
|
+
return hit;
|
|
7455
7584
|
}
|
|
7456
|
-
const zeroCall = rc.config.zeroCallEnabled !== false &&
|
|
7585
|
+
const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
|
|
7457
7586
|
conversationTokens: rc.convTokens,
|
|
7458
7587
|
toolPercent: rc.toolPercent
|
|
7459
7588
|
}) >= 0.85;
|
|
7460
7589
|
if (zeroCall) {
|
|
7590
|
+
if (!rc.flags.autoTriggered)
|
|
7591
|
+
showProgressOverlay(rc.ctx, {
|
|
7592
|
+
phase: 3,
|
|
7593
|
+
phaseName: "Synthesize",
|
|
7594
|
+
detail: "Building a deterministic continuation summary \xB7 no LLM call"
|
|
7595
|
+
});
|
|
7461
7596
|
const finalSummary2 = assembleFallback([], extraction);
|
|
7462
7597
|
setCachedSynthesis(cacheKey, {
|
|
7463
7598
|
finalSummary: finalSummary2,
|
|
@@ -7468,7 +7603,7 @@ async function summarizeConversation(rc) {
|
|
|
7468
7603
|
chunkCount: 0
|
|
7469
7604
|
});
|
|
7470
7605
|
rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
|
|
7471
|
-
const deterministic = rc;
|
|
7606
|
+
const deterministic = advance(rc, "_synthesized");
|
|
7472
7607
|
deterministic.finalSummary = finalSummary2;
|
|
7473
7608
|
deterministic.method = "heuristic";
|
|
7474
7609
|
deterministic.methodForMetrics = "zero-call";
|
|
@@ -7478,7 +7613,7 @@ async function summarizeConversation(rc) {
|
|
|
7478
7613
|
deterministic.explorationRounds = 0;
|
|
7479
7614
|
deterministic.chunkCount = 0;
|
|
7480
7615
|
markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
|
|
7481
|
-
return
|
|
7616
|
+
return deterministic;
|
|
7482
7617
|
}
|
|
7483
7618
|
const shouldSkipExplore = !policy.explore;
|
|
7484
7619
|
const convText = rc.convText;
|
|
@@ -7494,9 +7629,16 @@ async function summarizeConversation(rc) {
|
|
|
7494
7629
|
try {
|
|
7495
7630
|
summaryAuth = await resolveStageAuth(rc, "summary");
|
|
7496
7631
|
} catch (error2) {
|
|
7497
|
-
|
|
7632
|
+
debugError("Summary route unavailable", error2);
|
|
7633
|
+
rc.notify("Summary route unavailable \xB7 using deterministic fallback", "info");
|
|
7498
7634
|
}
|
|
7499
7635
|
if (!summaryAuth) {
|
|
7636
|
+
if (!rc.flags.autoTriggered)
|
|
7637
|
+
showProgressOverlay(rc.ctx, {
|
|
7638
|
+
phase: 3,
|
|
7639
|
+
phaseName: "Synthesize",
|
|
7640
|
+
detail: "Summary route unavailable \xB7 building a deterministic summary"
|
|
7641
|
+
});
|
|
7500
7642
|
finalSummary = assembleFallback([], extraction);
|
|
7501
7643
|
method = "heuristic";
|
|
7502
7644
|
} else if (rc.convTokens < singlePassMaxTokens) {
|
|
@@ -7504,7 +7646,7 @@ async function summarizeConversation(rc) {
|
|
|
7504
7646
|
showProgressOverlay(rc.ctx, {
|
|
7505
7647
|
phase: 3,
|
|
7506
7648
|
phaseName: "Synthesize",
|
|
7507
|
-
detail: "
|
|
7649
|
+
detail: "Writing one continuation summary from " + rc.convTokens.toLocaleString() + " tokens",
|
|
7508
7650
|
model: rc.modelLabel,
|
|
7509
7651
|
profile: rc.profile,
|
|
7510
7652
|
extraction
|
|
@@ -7515,7 +7657,8 @@ async function summarizeConversation(rc) {
|
|
|
7515
7657
|
finalSummary = r.summary;
|
|
7516
7658
|
method = "single-pass";
|
|
7517
7659
|
} catch (err) {
|
|
7518
|
-
|
|
7660
|
+
debugError("Single-pass synthesis used deterministic fallback", err);
|
|
7661
|
+
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback", "info");
|
|
7519
7662
|
finalSummary = assembleFallback([], extraction);
|
|
7520
7663
|
method = "heuristic";
|
|
7521
7664
|
}
|
|
@@ -7527,7 +7670,7 @@ async function summarizeConversation(rc) {
|
|
|
7527
7670
|
showProgressOverlay(rc.ctx, {
|
|
7528
7671
|
phase: 2,
|
|
7529
7672
|
phaseName: "Explore",
|
|
7530
|
-
detail: "
|
|
7673
|
+
detail: "Mapping topic shifts and continuity risks",
|
|
7531
7674
|
model: rc.modelLabel,
|
|
7532
7675
|
profile: rc.profile,
|
|
7533
7676
|
extraction
|
|
@@ -7542,7 +7685,8 @@ async function summarizeConversation(rc) {
|
|
|
7542
7685
|
rc.notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
7543
7686
|
rc.vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
|
|
7544
7687
|
} catch (err) {
|
|
7545
|
-
|
|
7688
|
+
debugError("Explore used deterministic topic boundaries", err);
|
|
7689
|
+
rc.notify("Explore unavailable \xB7 using deterministic topic boundaries", "info");
|
|
7546
7690
|
} finally {
|
|
7547
7691
|
const exploreEnd = Date.now();
|
|
7548
7692
|
markMeasuredPhase(rc, "explore", exploreStart, exploreEnd);
|
|
@@ -7589,7 +7733,7 @@ async function summarizeConversation(rc) {
|
|
|
7589
7733
|
showProgressOverlay(rc.ctx, {
|
|
7590
7734
|
phase: 3,
|
|
7591
7735
|
phaseName: "Synthesize",
|
|
7592
|
-
detail: "0/" + totalBatches
|
|
7736
|
+
detail: "Compressing older history \xB7 batch 0/" + totalBatches,
|
|
7593
7737
|
model: rc.modelLabel,
|
|
7594
7738
|
profile: rc.profile,
|
|
7595
7739
|
extraction,
|
|
@@ -7629,7 +7773,7 @@ async function summarizeConversation(rc) {
|
|
|
7629
7773
|
for (let index = wave;index < totalBatches; index++) {
|
|
7630
7774
|
results[index] = batches[index].map((chunk) => failedChunkSummary(chunk));
|
|
7631
7775
|
}
|
|
7632
|
-
rc.notify("Synthesis budget
|
|
7776
|
+
rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
|
|
7633
7777
|
break;
|
|
7634
7778
|
}
|
|
7635
7779
|
const waveBatches = batches.slice(wave, Math.min(wave + concurrency, batchCallLimit));
|
|
@@ -7646,7 +7790,7 @@ async function summarizeConversation(rc) {
|
|
|
7646
7790
|
showProgressOverlay(rc.ctx, {
|
|
7647
7791
|
phase: 3,
|
|
7648
7792
|
phaseName: "Synthesize",
|
|
7649
|
-
detail: completed + "/" + totalBatches
|
|
7793
|
+
detail: "Compressing older history \xB7 batch " + completed + "/" + totalBatches,
|
|
7650
7794
|
model: rc.modelLabel,
|
|
7651
7795
|
profile: rc.profile,
|
|
7652
7796
|
extraction,
|
|
@@ -7661,15 +7805,25 @@ async function summarizeConversation(rc) {
|
|
|
7661
7805
|
for (const r of results)
|
|
7662
7806
|
if (r)
|
|
7663
7807
|
summaries.push(...r);
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7808
|
+
const failedBatches = errors.filter(Boolean);
|
|
7809
|
+
for (const error2 of failedBatches)
|
|
7810
|
+
debugError("Synthesis batch used deterministic fallback", error2);
|
|
7811
|
+
if (failedBatches.length) {
|
|
7812
|
+
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage", "info");
|
|
7813
|
+
if (!rc.flags.autoTriggered)
|
|
7814
|
+
showProgressOverlay(rc.ctx, {
|
|
7815
|
+
phase: 3,
|
|
7816
|
+
phaseName: "Synthesize",
|
|
7817
|
+
detail: failedBatches.length + " batch fallback(s) \xB7 preserving coverage from deterministic evidence",
|
|
7818
|
+
explorationRounds
|
|
7819
|
+
});
|
|
7820
|
+
}
|
|
7667
7821
|
}
|
|
7668
7822
|
if (!rc.flags.autoTriggered) {
|
|
7669
7823
|
showProgressOverlay(rc.ctx, {
|
|
7670
7824
|
phase: 3,
|
|
7671
7825
|
phaseName: "Synthesize",
|
|
7672
|
-
detail: "
|
|
7826
|
+
detail: "Merging summaries with project continuity",
|
|
7673
7827
|
model: rc.modelLabel,
|
|
7674
7828
|
profile: rc.profile,
|
|
7675
7829
|
extraction,
|
|
@@ -7684,12 +7838,12 @@ async function summarizeConversation(rc) {
|
|
|
7684
7838
|
else
|
|
7685
7839
|
throw new Error("bad");
|
|
7686
7840
|
} catch (err) {
|
|
7687
|
-
|
|
7841
|
+
debugError("Assembly used deterministic fallback", err);
|
|
7688
7842
|
finalSummary = assembleFallback(summaries, extraction);
|
|
7689
7843
|
}
|
|
7690
7844
|
method = "eesv";
|
|
7691
7845
|
}
|
|
7692
|
-
const out = rc;
|
|
7846
|
+
const out = advance(rc, "_synthesized");
|
|
7693
7847
|
out.finalSummary = finalSummary;
|
|
7694
7848
|
out.method = method;
|
|
7695
7849
|
out.methodForMetrics = method;
|
|
@@ -7707,7 +7861,7 @@ async function summarizeConversation(rc) {
|
|
|
7707
7861
|
chunkCount
|
|
7708
7862
|
});
|
|
7709
7863
|
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
7710
|
-
return
|
|
7864
|
+
return out;
|
|
7711
7865
|
}
|
|
7712
7866
|
|
|
7713
7867
|
// src/phases/verify.ts
|
|
@@ -7781,6 +7935,17 @@ var CONDITION_MARKERS = new Set([
|
|
|
7781
7935
|
"gerekli",
|
|
7782
7936
|
"gerektirir"
|
|
7783
7937
|
]);
|
|
7938
|
+
var POLARITY_INVERTING_GUARDS = new Set([
|
|
7939
|
+
"skip",
|
|
7940
|
+
"skipp",
|
|
7941
|
+
"forget",
|
|
7942
|
+
"forgett",
|
|
7943
|
+
"omit",
|
|
7944
|
+
"omitt",
|
|
7945
|
+
"neglect",
|
|
7946
|
+
"fail",
|
|
7947
|
+
"avoid"
|
|
7948
|
+
]);
|
|
7784
7949
|
var SEMANTIC_STOP = new Set([
|
|
7785
7950
|
"the",
|
|
7786
7951
|
"and",
|
|
@@ -7830,6 +7995,24 @@ function evidenceFragments(text) {
|
|
|
7830
7995
|
function hasNearbyMarker(tokens, anchor, markers) {
|
|
7831
7996
|
return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
|
|
7832
7997
|
}
|
|
7998
|
+
function hasEffectiveTargetNegation(tokens, anchor) {
|
|
7999
|
+
return tokens.some((token, anchorIndex) => {
|
|
8000
|
+
if (token !== anchor)
|
|
8001
|
+
return false;
|
|
8002
|
+
const nearbyStart = Math.max(0, anchorIndex - 2);
|
|
8003
|
+
const nearbyNegations = tokens.slice(nearbyStart, anchorIndex + 3).map((near, offset) => NEGATION_MARKERS.has(near) ? nearbyStart + offset : -1).filter((index) => index >= 0);
|
|
8004
|
+
const governingStart = Math.max(0, anchorIndex - 3);
|
|
8005
|
+
const preceding = tokens.slice(governingStart, anchorIndex);
|
|
8006
|
+
const nearbyGuards = preceding.map((near, offset) => POLARITY_INVERTING_GUARDS.has(near) ? governingStart + offset : -1).filter((index) => index >= 0);
|
|
8007
|
+
const governingIndex = preceding.findIndex((near, offset) => NEGATION_MARKERS.has(near) && POLARITY_INVERTING_GUARDS.has(preceding[offset + 1] ?? ""));
|
|
8008
|
+
if (governingIndex < 0)
|
|
8009
|
+
return nearbyNegations.length > 0 || nearbyGuards.length > 0;
|
|
8010
|
+
const absoluteGoverningIndex = governingStart + governingIndex;
|
|
8011
|
+
const guardIndex = absoluteGoverningIndex + 1;
|
|
8012
|
+
const nested = tokens.slice(guardIndex + 1, anchorIndex).some((inner) => NEGATION_MARKERS.has(inner) || POLARITY_INVERTING_GUARDS.has(inner));
|
|
8013
|
+
return nested || nearbyNegations.some((index) => index !== absoluteGoverningIndex && index !== guardIndex) || nearbyGuards.some((index) => index !== guardIndex);
|
|
8014
|
+
});
|
|
8015
|
+
}
|
|
7833
8016
|
function semanticShape(source) {
|
|
7834
8017
|
const sourceTokens = semanticTokens(source);
|
|
7835
8018
|
const concepts = Array.from(new Set(sourceTokens.filter((token) => !/^\d+$/.test(token) && !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
|
|
@@ -7848,11 +8031,14 @@ function hasSemanticEvidence(source, target) {
|
|
|
7848
8031
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7849
8032
|
if (overlap < required)
|
|
7850
8033
|
return false;
|
|
7851
|
-
|
|
8034
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
8035
|
+
if (negative && !targetNegative) {
|
|
7852
8036
|
const conditionalRestatement = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
7853
8037
|
if (!conditionalRestatement)
|
|
7854
8038
|
return false;
|
|
7855
8039
|
}
|
|
8040
|
+
if (!negative && targetNegative)
|
|
8041
|
+
return false;
|
|
7856
8042
|
if (conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token)))
|
|
7857
8043
|
return false;
|
|
7858
8044
|
return true;
|
|
@@ -7870,10 +8056,13 @@ function hasSemanticContradiction(source, target) {
|
|
|
7870
8056
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7871
8057
|
if (overlap < required)
|
|
7872
8058
|
return false;
|
|
7873
|
-
|
|
8059
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
8060
|
+
if (negative && !targetNegative) {
|
|
7874
8061
|
const validConditional = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
7875
8062
|
return !validConditional;
|
|
7876
8063
|
}
|
|
8064
|
+
if (!negative && targetNegative)
|
|
8065
|
+
return true;
|
|
7877
8066
|
return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
|
|
7878
8067
|
});
|
|
7879
8068
|
}
|
|
@@ -7955,7 +8144,7 @@ function verifySummary(summary, extraction, continuity = null) {
|
|
|
7955
8144
|
}
|
|
7956
8145
|
}
|
|
7957
8146
|
for (const error2 of unresolvedEvidence) {
|
|
7958
|
-
const snippet = error2.message
|
|
8147
|
+
const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
7959
8148
|
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
7960
8149
|
gaps.push({ kind: "missing-error", message: error2.message });
|
|
7961
8150
|
score -= 5;
|
|
@@ -8182,7 +8371,7 @@ async function verifyAndPatch(rc) {
|
|
|
8182
8371
|
showProgressOverlay(rc.ctx, {
|
|
8183
8372
|
phase: 4,
|
|
8184
8373
|
phaseName: "Verify",
|
|
8185
|
-
detail: "Checking
|
|
8374
|
+
detail: "Checking facts, files, constraints, errors, and open loops",
|
|
8186
8375
|
model: rc.modelLabel,
|
|
8187
8376
|
profile: rc.profile,
|
|
8188
8377
|
extraction,
|
|
@@ -8197,7 +8386,14 @@ async function verifyAndPatch(rc) {
|
|
|
8197
8386
|
rc.vlog("Verification score=" + verification.score + " ok=" + verification.ok + " gaps=" + verification.gaps.length);
|
|
8198
8387
|
const initialPatchable = verification.gaps.filter(isDeterministicallyPatchable);
|
|
8199
8388
|
if (initialPatchable.length > 0) {
|
|
8200
|
-
rc.notify("Phase 4 Verify: " + initialPatchable.length + " deterministic gap(s), score=" + verification.score + ", applying repair", "
|
|
8389
|
+
rc.notify("Phase 4 Verify: " + initialPatchable.length + " deterministic gap(s), score=" + verification.score + ", applying repair", "info");
|
|
8390
|
+
if (!rc.flags.autoTriggered)
|
|
8391
|
+
showProgressOverlay(rc.ctx, {
|
|
8392
|
+
phase: 4,
|
|
8393
|
+
phaseName: "Verify",
|
|
8394
|
+
detail: "Repairing " + initialPatchable.length + " deterministic finding(s)",
|
|
8395
|
+
explorationRounds: rc.explorationRounds
|
|
8396
|
+
});
|
|
8201
8397
|
const repaired = repairSummaryDeterministically(summary, verification, extraction, rc.previousState);
|
|
8202
8398
|
summary = repaired.summary;
|
|
8203
8399
|
verification = repaired.result;
|
|
@@ -8205,13 +8401,20 @@ async function verifyAndPatch(rc) {
|
|
|
8205
8401
|
}
|
|
8206
8402
|
const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
|
|
8207
8403
|
if (MODE_POLICIES[mode].allowLlmPatch && !verification.ok) {
|
|
8208
|
-
rc.notify("Phase 4 Verify: deterministic repair insufficient (score=" + verification.score + "), requesting LLM patch", "
|
|
8404
|
+
rc.notify("Phase 4 Verify: deterministic repair insufficient (score=" + verification.score + "), requesting LLM patch", "info");
|
|
8405
|
+
if (!rc.flags.autoTriggered)
|
|
8406
|
+
showProgressOverlay(rc.ctx, {
|
|
8407
|
+
phase: 4,
|
|
8408
|
+
phaseName: "Verify",
|
|
8409
|
+
detail: "Requesting a semantic repair for unresolved findings",
|
|
8410
|
+
explorationRounds: rc.explorationRounds
|
|
8411
|
+
});
|
|
8209
8412
|
const beforePatch = summary;
|
|
8210
8413
|
try {
|
|
8211
8414
|
const verifyAuth = await resolveStageAuth(rc, "verify");
|
|
8212
8415
|
summary = await patchSummary(summary, verification.gaps, rc.verifyModel ?? rc.summaryModel, verifyAuth, rc.cancellation.signal, rc.services);
|
|
8213
8416
|
} catch (error2) {
|
|
8214
|
-
|
|
8417
|
+
debugError("LLM verification patch used deterministic fallback", error2);
|
|
8215
8418
|
}
|
|
8216
8419
|
if (summary !== beforePatch) {
|
|
8217
8420
|
llmPatched = true;
|
|
@@ -8223,6 +8426,13 @@ async function verifyAndPatch(rc) {
|
|
|
8223
8426
|
}
|
|
8224
8427
|
}
|
|
8225
8428
|
if (!verification.ok) {
|
|
8429
|
+
if (!rc.flags.autoTriggered)
|
|
8430
|
+
showProgressOverlay(rc.ctx, {
|
|
8431
|
+
phase: 4,
|
|
8432
|
+
phaseName: "Verify",
|
|
8433
|
+
detail: "Trying the deterministic safety summary",
|
|
8434
|
+
explorationRounds: rc.explorationRounds
|
|
8435
|
+
});
|
|
8226
8436
|
let deterministic = assembleFallback(rc.summaries, extraction);
|
|
8227
8437
|
let deterministicVerification = verifySummary(deterministic, extraction, rc.previousState);
|
|
8228
8438
|
const repaired = repairSummaryDeterministically(deterministic, deterministicVerification, extraction, rc.previousState);
|
|
@@ -8233,12 +8443,19 @@ async function verifyAndPatch(rc) {
|
|
|
8233
8443
|
verification = deterministicVerification;
|
|
8234
8444
|
deterministicPatched.push(...repaired.patched);
|
|
8235
8445
|
qualityFloorUsed = true;
|
|
8236
|
-
rc.notify("Quality floor replaced unsafe or unverifiable model output", "
|
|
8446
|
+
rc.notify("Quality floor replaced unsafe or unverifiable model output", "info");
|
|
8237
8447
|
}
|
|
8238
8448
|
}
|
|
8239
8449
|
const failure = verificationFailureMessage(verification);
|
|
8240
8450
|
if (failure)
|
|
8241
8451
|
throw new VerificationGateError(verification, initialScore);
|
|
8452
|
+
if (!rc.flags.autoTriggered)
|
|
8453
|
+
showProgressOverlay(rc.ctx, {
|
|
8454
|
+
phase: 4,
|
|
8455
|
+
phaseName: "Verify",
|
|
8456
|
+
detail: "Passed " + verification.score + "/100 \xB7 0 unresolved gaps",
|
|
8457
|
+
explorationRounds: rc.explorationRounds
|
|
8458
|
+
});
|
|
8242
8459
|
const out = rc;
|
|
8243
8460
|
out.finalSummary = summary;
|
|
8244
8461
|
out.verified = verification.ok;
|
|
@@ -8255,6 +8472,10 @@ async function verifyAndPatch(rc) {
|
|
|
8255
8472
|
return advance(out, "_verified");
|
|
8256
8473
|
}
|
|
8257
8474
|
|
|
8475
|
+
// src/app/steps/state.ts
|
|
8476
|
+
import fs7 from "fs";
|
|
8477
|
+
import path10 from "path";
|
|
8478
|
+
|
|
8258
8479
|
// src/domain/yield-gate.ts
|
|
8259
8480
|
class YieldGateError extends Error {
|
|
8260
8481
|
reason;
|
|
@@ -8335,9 +8556,14 @@ function buildState(rc) {
|
|
|
8335
8556
|
const nextActions = extractNextActions(summary);
|
|
8336
8557
|
const criticalContextItems = extractCriticalContext(summary);
|
|
8337
8558
|
const currentState = buildCompactionState(extraction, managedLoops, rc.explorationReport, nextActions, criticalContextItems, loopOverrides);
|
|
8559
|
+
const summarizedGoal = summaryEvidenceLine(findSection(summary, "goal")?.body ?? "", TRUNC.MESSAGE);
|
|
8338
8560
|
currentState.scope = rc.continuityScope;
|
|
8339
8561
|
currentState.factOverrides = prevState?.factOverrides ?? [];
|
|
8340
8562
|
let compactionState = mergeCompactionStates(prevState, currentState);
|
|
8563
|
+
compactionState.deletedFiles = compactionState.deletedFiles.filter((file) => {
|
|
8564
|
+
const candidate = path10.isAbsolute(file) ? file : path10.resolve(rc.ctx.cwd, file);
|
|
8565
|
+
return !fs7.existsSync(candidate);
|
|
8566
|
+
});
|
|
8341
8567
|
if (preserve.length > 0) {
|
|
8342
8568
|
compactionState.readFiles = Array.from(new Set([...preserve, ...compactionState.readFiles])).slice(0, 100);
|
|
8343
8569
|
}
|
|
@@ -8350,6 +8576,8 @@ function buildState(rc) {
|
|
|
8350
8576
|
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
8351
8577
|
}
|
|
8352
8578
|
}
|
|
8579
|
+
if (summarizedGoal && currentState.goal)
|
|
8580
|
+
compactionState.goal = summarizedGoal;
|
|
8353
8581
|
const continuity = renderContinuityCapsule(compactionState, undefined, summary);
|
|
8354
8582
|
if (continuity)
|
|
8355
8583
|
summary = summary.trimEnd() + `
|
|
@@ -8423,8 +8651,8 @@ function buildState(rc) {
|
|
|
8423
8651
|
|
|
8424
8652
|
// src/infra/context-graph.ts
|
|
8425
8653
|
import { createHash as createHash3 } from "crypto";
|
|
8426
|
-
import
|
|
8427
|
-
import
|
|
8654
|
+
import fs8 from "fs";
|
|
8655
|
+
import path11 from "path";
|
|
8428
8656
|
import { createRequire } from "module";
|
|
8429
8657
|
var require2 = createRequire(import.meta.url);
|
|
8430
8658
|
var MAX_PROJECT_NODES = 2000;
|
|
@@ -8432,6 +8660,7 @@ var MAX_MANUAL_NODES = 500;
|
|
|
8432
8660
|
var MAX_SESSION_NODES = 256;
|
|
8433
8661
|
var MAX_QUERY_CANDIDATES = 80;
|
|
8434
8662
|
var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
8663
|
+
var CONTEXT_GRAPH_SCHEMA_VERSION = 1;
|
|
8435
8664
|
function nodeSqliteAdapter(db) {
|
|
8436
8665
|
return {
|
|
8437
8666
|
exec: (sql) => db.exec(sql),
|
|
@@ -8454,7 +8683,7 @@ function nodeSqliteAdapter(db) {
|
|
|
8454
8683
|
}
|
|
8455
8684
|
function openDatabase() {
|
|
8456
8685
|
const fp = contextGraphFile();
|
|
8457
|
-
|
|
8686
|
+
fs8.mkdirSync(path11.dirname(fp), { recursive: true });
|
|
8458
8687
|
let db;
|
|
8459
8688
|
if ("bun" in process.versions) {
|
|
8460
8689
|
const { Database } = require2("bun:sqlite");
|
|
@@ -8464,7 +8693,7 @@ function openDatabase() {
|
|
|
8464
8693
|
db = nodeSqliteAdapter(new DatabaseSync(fp));
|
|
8465
8694
|
}
|
|
8466
8695
|
try {
|
|
8467
|
-
|
|
8696
|
+
fs8.chmodSync(fp, 384);
|
|
8468
8697
|
} catch {}
|
|
8469
8698
|
db.exec("PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;");
|
|
8470
8699
|
db.exec(`
|
|
@@ -8484,8 +8713,6 @@ function openDatabase() {
|
|
|
8484
8713
|
created_at INTEGER NOT NULL,
|
|
8485
8714
|
updated_at INTEGER NOT NULL
|
|
8486
8715
|
);
|
|
8487
|
-
CREATE UNIQUE INDEX IF NOT EXISTS context_nodes_fact
|
|
8488
|
-
ON context_nodes(project_id, session_id, kind, fact_key);
|
|
8489
8716
|
CREATE INDEX IF NOT EXISTS context_nodes_project_status
|
|
8490
8717
|
ON context_nodes(project_id, status, updated_at DESC);
|
|
8491
8718
|
CREATE TABLE IF NOT EXISTS context_edges (
|
|
@@ -8503,6 +8730,20 @@ function openDatabase() {
|
|
|
8503
8730
|
tokenize='unicode61 remove_diacritics 2'
|
|
8504
8731
|
);
|
|
8505
8732
|
`);
|
|
8733
|
+
const version = db.query("PRAGMA user_version").get();
|
|
8734
|
+
if (Number(version?.user_version ?? 0) < CONTEXT_GRAPH_SCHEMA_VERSION) {
|
|
8735
|
+
db.transaction(() => {
|
|
8736
|
+
db.exec(`
|
|
8737
|
+
DROP INDEX IF EXISTS context_nodes_fact;
|
|
8738
|
+
DELETE FROM context_nodes_fts
|
|
8739
|
+
WHERE node_id IN (SELECT id FROM context_nodes WHERE source = 'compaction');
|
|
8740
|
+
DELETE FROM context_nodes WHERE source = 'compaction';
|
|
8741
|
+
CREATE UNIQUE INDEX context_nodes_fact
|
|
8742
|
+
ON context_nodes(project_id, session_id, kind, fact_key, COALESCE(branch_head_id, ''));
|
|
8743
|
+
PRAGMA user_version = ${CONTEXT_GRAPH_SCHEMA_VERSION};
|
|
8744
|
+
`);
|
|
8745
|
+
})();
|
|
8746
|
+
}
|
|
8506
8747
|
return db;
|
|
8507
8748
|
}
|
|
8508
8749
|
function stableId(...parts) {
|
|
@@ -8552,7 +8793,7 @@ function makeNode(scope, kind, title, content, options = {}) {
|
|
|
8552
8793
|
const key = factKey(content);
|
|
8553
8794
|
const now = Date.now();
|
|
8554
8795
|
return {
|
|
8555
|
-
id: stableId(scope.projectId, scope.sessionId, kind, key),
|
|
8796
|
+
id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
|
|
8556
8797
|
projectId: scope.projectId,
|
|
8557
8798
|
sessionId: scope.sessionId,
|
|
8558
8799
|
branchHeadId: scope.branchHeadId ?? null,
|
|
@@ -8579,27 +8820,55 @@ function ensureFileNode(db, scope, file, now, content = file) {
|
|
|
8579
8820
|
upsertNode(db, node);
|
|
8580
8821
|
return node;
|
|
8581
8822
|
}
|
|
8582
|
-
function
|
|
8823
|
+
function branchLineage(scope) {
|
|
8824
|
+
return Array.from(new Set([...scope.branchEntryIds ?? [], scope.branchHeadId].filter((id) => typeof id === "string" && id.length > 0)));
|
|
8825
|
+
}
|
|
8826
|
+
function lineageFactRows(db, scope, kind, key) {
|
|
8827
|
+
const lineage = new Set(branchLineage(scope));
|
|
8583
8828
|
const rows = db.query(`
|
|
8584
|
-
SELECT
|
|
8585
|
-
WHERE project_id = ? AND session_id = ? AND
|
|
8586
|
-
|
|
8587
|
-
|
|
8829
|
+
SELECT * FROM context_nodes
|
|
8830
|
+
WHERE project_id = ? AND session_id = ? AND source = 'compaction'
|
|
8831
|
+
${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
|
|
8832
|
+
`).all(scope.projectId, scope.sessionId, ...kind ? [kind] : [], ...key ? [key] : []);
|
|
8833
|
+
return rows.filter((row) => lineage.size > 0 ? Boolean(row.branch_head_id && lineage.has(row.branch_head_id)) : row.branch_head_id == null);
|
|
8834
|
+
}
|
|
8835
|
+
function latestLineageFact(db, scope, kind, key) {
|
|
8836
|
+
const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
|
|
8837
|
+
return lineageFactRows(db, scope, kind, key).sort((a, b) => (rank.get(b.branch_head_id ?? "") ?? -1) - (rank.get(a.branch_head_id ?? "") ?? -1) || b.updated_at - a.updated_at)[0] ?? null;
|
|
8838
|
+
}
|
|
8839
|
+
function markFactStatus(db, scope, kind, key, status) {
|
|
8840
|
+
const previous = latestLineageFact(db, scope, kind, key);
|
|
8841
|
+
if (!previous || previous.status === status)
|
|
8588
8842
|
return;
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8843
|
+
const now = Date.now();
|
|
8844
|
+
upsertNode(db, {
|
|
8845
|
+
id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
|
|
8846
|
+
projectId: scope.projectId,
|
|
8847
|
+
sessionId: scope.sessionId,
|
|
8848
|
+
branchHeadId: scope.branchHeadId ?? null,
|
|
8849
|
+
kind,
|
|
8850
|
+
factKey: key,
|
|
8851
|
+
title: previous.title,
|
|
8852
|
+
content: previous.content,
|
|
8853
|
+
status,
|
|
8854
|
+
source: "compaction",
|
|
8855
|
+
confidence: previous.confidence,
|
|
8856
|
+
relatedPaths: parsePaths(previous.related_paths),
|
|
8857
|
+
createdAt: now,
|
|
8858
|
+
updatedAt: now
|
|
8859
|
+
}, true);
|
|
8860
|
+
}
|
|
8861
|
+
function sameActiveFact(row, node) {
|
|
8862
|
+
return Boolean(row && row.status === "active" && node.status === "active" && row.title === node.title && row.content === node.content && row.confidence === node.confidence && JSON.stringify(parsePaths(row.related_paths)) === JSON.stringify(node.relatedPaths));
|
|
8596
8863
|
}
|
|
8597
8864
|
function addFact(db, scope, sessionNodeId, kind, title, content, relatedPaths = [], confidence = 0.85, keyText = content) {
|
|
8598
8865
|
if (!content.trim())
|
|
8599
8866
|
return;
|
|
8600
8867
|
const node = makeNode(scope, kind, title, content, { relatedPaths, confidence });
|
|
8601
8868
|
node.factKey = factKey(keyText);
|
|
8602
|
-
node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey);
|
|
8869
|
+
node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey, scope.branchHeadId ?? "");
|
|
8870
|
+
if (sameActiveFact(latestLineageFact(db, scope, kind, node.factKey), node))
|
|
8871
|
+
return;
|
|
8603
8872
|
upsertNode(db, node);
|
|
8604
8873
|
linkNodes(db, scope.projectId, sessionNodeId, node.id, "contains", 1, node.updatedAt);
|
|
8605
8874
|
for (const file of relatedPaths) {
|
|
@@ -8640,7 +8909,8 @@ function indexCompactionState(projectId, state) {
|
|
|
8640
8909
|
const scope = {
|
|
8641
8910
|
projectId,
|
|
8642
8911
|
sessionId,
|
|
8643
|
-
branchHeadId: state.scope.branchHeadId
|
|
8912
|
+
branchHeadId: state.scope.branchHeadId,
|
|
8913
|
+
branchEntryIds: state.scope.branchAncestryIds
|
|
8644
8914
|
};
|
|
8645
8915
|
let db = null;
|
|
8646
8916
|
try {
|
|
@@ -8656,17 +8926,15 @@ function indexCompactionState(projectId, state) {
|
|
|
8656
8926
|
upsertNode(db, projectNode);
|
|
8657
8927
|
upsertNode(db, sessionNode);
|
|
8658
8928
|
linkNodes(db, projectId, projectNode.id, sessionNode.id, "contains", 1, now);
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
)
|
|
8667
|
-
`).run(projectId, sessionId);
|
|
8668
|
-
if (state.goal)
|
|
8929
|
+
if (state.goal) {
|
|
8930
|
+
const currentGoalKey = factKey(state.goal);
|
|
8931
|
+
const priorGoalKeys = new Set(lineageFactRows(db, scope, "goal").map((row) => row.fact_key));
|
|
8932
|
+
for (const key of priorGoalKeys) {
|
|
8933
|
+
if (key !== currentGoalKey)
|
|
8934
|
+
markFactStatus(db, scope, "goal", key, "superseded");
|
|
8935
|
+
}
|
|
8669
8936
|
addFact(db, scope, sessionNode.id, "goal", "Current goal", state.goal, [], 0.98);
|
|
8937
|
+
}
|
|
8670
8938
|
for (const item of state.decisions) {
|
|
8671
8939
|
addFact(db, scope, sessionNode.id, "decision", "Decision", item.summary + (item.userResponse ? " \u2192 " + item.userResponse : ""), [], item.type === "explicit" ? 0.98 : 0.82, item.summary);
|
|
8672
8940
|
}
|
|
@@ -8685,6 +8953,8 @@ function indexCompactionState(projectId, state) {
|
|
|
8685
8953
|
relatedPaths: item.files,
|
|
8686
8954
|
confidence: item.priority === "critical" || item.priority === "high" ? 0.98 : 0.88
|
|
8687
8955
|
});
|
|
8956
|
+
if (sameActiveFact(latestLineageFact(db, scope, "loop", node.factKey), node))
|
|
8957
|
+
continue;
|
|
8688
8958
|
upsertNode(db, node);
|
|
8689
8959
|
linkNodes(db, projectId, sessionNode.id, node.id, "contains", 1, now);
|
|
8690
8960
|
for (const file of item.files) {
|
|
@@ -8805,13 +9075,17 @@ function saveContextMemory(scope, memory) {
|
|
|
8805
9075
|
node.sessionId = "*";
|
|
8806
9076
|
node.branchHeadId = null;
|
|
8807
9077
|
const transaction = db.transaction(() => {
|
|
8808
|
-
const
|
|
9078
|
+
const existing = db.query("SELECT status FROM context_nodes WHERE id = ?").get(node.id);
|
|
8809
9079
|
const duplicates = db.query(`
|
|
8810
|
-
SELECT id, related_paths FROM context_nodes
|
|
9080
|
+
SELECT id, related_paths, status FROM context_nodes
|
|
8811
9081
|
WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND id <> ?
|
|
8812
9082
|
`).all(scope.projectId, memory.kind, node.factKey, node.id);
|
|
8813
|
-
const count = db.query(
|
|
8814
|
-
|
|
9083
|
+
const count = db.query(`
|
|
9084
|
+
SELECT count(*) AS count FROM context_nodes
|
|
9085
|
+
WHERE project_id = ? AND source = 'manual' AND status = 'active'
|
|
9086
|
+
`).get(scope.projectId);
|
|
9087
|
+
const alreadyActive = existing?.status === "active" || duplicates.some((item) => item.status === "active");
|
|
9088
|
+
if (!alreadyActive && Number(count?.count ?? 0) >= MAX_MANUAL_NODES) {
|
|
8815
9089
|
throw new Error("Project memory limit reached; resolve an existing memory before saving another");
|
|
8816
9090
|
}
|
|
8817
9091
|
node.relatedPaths = Array.from(new Set([
|
|
@@ -8898,6 +9172,19 @@ function parsePaths(value) {
|
|
|
8898
9172
|
return [];
|
|
8899
9173
|
}
|
|
8900
9174
|
}
|
|
9175
|
+
function latestLineageVersions(db, scope) {
|
|
9176
|
+
const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
|
|
9177
|
+
const latest = new Map;
|
|
9178
|
+
for (const row of lineageFactRows(db, scope)) {
|
|
9179
|
+
const key = row.kind + ":" + row.fact_key;
|
|
9180
|
+
const rowRank = rank.get(row.branch_head_id ?? "") ?? -1;
|
|
9181
|
+
const previous = latest.get(key);
|
|
9182
|
+
if (!previous || rowRank > previous.rank || rowRank === previous.rank && row.updated_at > previous.updatedAt) {
|
|
9183
|
+
latest.set(key, { id: row.id, status: row.status, rank: rowRank, updatedAt: row.updated_at });
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
return new Map([...latest].map(([key, value]) => [key, { id: value.id, status: value.status }]));
|
|
9187
|
+
}
|
|
8901
9188
|
function recallContext(scope, query, options = {}) {
|
|
8902
9189
|
const terms = searchTerms(query.slice(0, 500));
|
|
8903
9190
|
if (!terms.length)
|
|
@@ -8920,7 +9207,8 @@ function recallContext(scope, query, options = {}) {
|
|
|
8920
9207
|
candidates.set(neighbor.row.id, { row: neighbor.row, lexical: 0, graph: neighbor.weight });
|
|
8921
9208
|
}
|
|
8922
9209
|
const allowedKinds = options.kinds?.length ? new Set(options.kinds) : null;
|
|
8923
|
-
const branchIds = new Set(scope
|
|
9210
|
+
const branchIds = new Set(branchLineage(scope));
|
|
9211
|
+
const latestVersions = latestLineageVersions(db, scope);
|
|
8924
9212
|
const kindBoost = {
|
|
8925
9213
|
decision: 0.1,
|
|
8926
9214
|
constraint: 0.1,
|
|
@@ -8939,6 +9227,11 @@ function recallContext(scope, query, options = {}) {
|
|
|
8939
9227
|
return [];
|
|
8940
9228
|
if (allowedKinds && !allowedKinds.has(row.kind))
|
|
8941
9229
|
return [];
|
|
9230
|
+
if (row.source === "compaction" && sameSession && sameBranch) {
|
|
9231
|
+
const latest = latestVersions.get(row.kind + ":" + row.fact_key);
|
|
9232
|
+
if (latest && (latest.status !== "active" || latest.id !== row.id))
|
|
9233
|
+
return [];
|
|
9234
|
+
}
|
|
8942
9235
|
const recency = Math.max(0, 1 - (now - row.updated_at) / NINETY_DAYS_MS);
|
|
8943
9236
|
const score = Math.min(1, 0.05 + lexical * 0.3 + graph * 0.15 + (sameBranch ? 0.4 : sameSession ? 0.05 : 0) + (kindBoost[row.kind] ?? 0.03) + Math.max(0, Math.min(1, row.confidence)) * 0.08 + recency * 0.05 + (row.source === "manual" ? 0.04 : 0));
|
|
8944
9237
|
return [{ row, score, sameSession, sameBranch }];
|
|
@@ -9266,7 +9559,7 @@ function runDamageDetection(rc) {
|
|
|
9266
9559
|
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
9267
9560
|
}
|
|
9268
9561
|
} catch (err) {
|
|
9269
|
-
|
|
9562
|
+
debugError("Damage detection skipped", err);
|
|
9270
9563
|
}
|
|
9271
9564
|
}
|
|
9272
9565
|
function stagePendingCompaction(rc, metricsSnapshot) {
|
|
@@ -9319,6 +9612,8 @@ function makeBase(opts) {
|
|
|
9319
9612
|
const notify = (msg, type = "info") => {
|
|
9320
9613
|
if (opts.autoTriggered && (type === "info" || type === "success"))
|
|
9321
9614
|
return;
|
|
9615
|
+
if (type === "info" && !opts.verbose)
|
|
9616
|
+
return;
|
|
9322
9617
|
opts.ctx.ui.notify(msg, type === "success" ? "info" : type);
|
|
9323
9618
|
};
|
|
9324
9619
|
const vlog = (msg) => {
|
|
@@ -9391,6 +9686,7 @@ async function runSmartCompact(opts) {
|
|
|
9391
9686
|
}
|
|
9392
9687
|
let finalRc = null;
|
|
9393
9688
|
let keepApplyProgress = false;
|
|
9689
|
+
let runFailed = false;
|
|
9394
9690
|
let failureSummaryFields = { profile: base.profile, mode: base.mode };
|
|
9395
9691
|
if (opts.cancellationOut) {
|
|
9396
9692
|
opts.cancellationOut.value = {
|
|
@@ -9412,7 +9708,7 @@ async function runSmartCompact(opts) {
|
|
|
9412
9708
|
const prepared = await prepareRun(base);
|
|
9413
9709
|
if (!prepared)
|
|
9414
9710
|
return;
|
|
9415
|
-
base.notify("EESV Compact (" + base.modelLabel + ", " + base.
|
|
9711
|
+
base.notify("EESV Compact (" + base.modelLabel + ", " + base.mode + ") \u2014 " + (base.ctx.getContextUsage()?.tokens ?? 0).toLocaleString() + "t", "info");
|
|
9416
9712
|
const windowed = resolveCompactionWindow(prepared);
|
|
9417
9713
|
if (!windowed)
|
|
9418
9714
|
return;
|
|
@@ -9424,7 +9720,13 @@ async function runSmartCompact(opts) {
|
|
|
9424
9720
|
};
|
|
9425
9721
|
markPhase(windowed, "prepare");
|
|
9426
9722
|
if (!windowed.flags.autoTriggered) {
|
|
9427
|
-
showProgressOverlay(windowed.ctx, {
|
|
9723
|
+
showProgressOverlay(windowed.ctx, {
|
|
9724
|
+
phase: 1,
|
|
9725
|
+
phaseName: "Extract",
|
|
9726
|
+
detail: "Indexing goals, files, decisions, errors, and open loops",
|
|
9727
|
+
model: windowed.modelLabel,
|
|
9728
|
+
profile: windowed.profile
|
|
9729
|
+
});
|
|
9428
9730
|
}
|
|
9429
9731
|
const recovered = recoverSessionLog(windowed);
|
|
9430
9732
|
markPhase(recovered, "recover");
|
|
@@ -9448,7 +9750,7 @@ async function runSmartCompact(opts) {
|
|
|
9448
9750
|
markPhase(stated, "state");
|
|
9449
9751
|
if (stated.flags.dryRun) {
|
|
9450
9752
|
recordSuccessMetrics(stated, "dry-run");
|
|
9451
|
-
stated.notify("DRY RUN (" + stated.method + ", " + stated.
|
|
9753
|
+
stated.ctx.ui.notify("DRY RUN (" + stated.method + ", " + stated.mode + ") \u2014 " + stated.toCompact.length + " msgs, " + stated.llmCalls + " calls", "info");
|
|
9452
9754
|
return;
|
|
9453
9755
|
}
|
|
9454
9756
|
if (stated.cancellation.timedOut)
|
|
@@ -9461,20 +9763,24 @@ async function runSmartCompact(opts) {
|
|
|
9461
9763
|
try {
|
|
9462
9764
|
decision = stated.ctx.hasUI ? await showResultScreen(stated.ctx, stated.details, stated.extraction, stated.services, { approval: true }) : "cancel";
|
|
9463
9765
|
} catch (err) {
|
|
9464
|
-
|
|
9766
|
+
debugError("Approval UI stopped", err);
|
|
9465
9767
|
stated.notify("Approval UI failed \u2014 compaction cancelled", "warning");
|
|
9466
9768
|
}
|
|
9467
9769
|
if (decision !== "apply") {
|
|
9468
9770
|
stated.pendingRef.clear(stated.sessionId);
|
|
9469
9771
|
recordSuccessMetrics(stated, "cancelled");
|
|
9470
|
-
stated.notify("Compaction cancelled \u2014 current conversation unchanged", "info");
|
|
9772
|
+
stated.ctx.ui.notify("Compaction cancelled \u2014 current conversation unchanged", "info");
|
|
9471
9773
|
return;
|
|
9472
9774
|
}
|
|
9473
9775
|
}
|
|
9474
9776
|
if (stated.cancellation.timedOut)
|
|
9475
9777
|
return;
|
|
9476
9778
|
if (willApply) {
|
|
9477
|
-
showProgressOverlay(stated.ctx, {
|
|
9779
|
+
showProgressOverlay(stated.ctx, {
|
|
9780
|
+
phase: 5,
|
|
9781
|
+
phaseName: "Apply",
|
|
9782
|
+
detail: "Verified " + stated.verificationScore + "/100 \xB7 staging this run \xB7 awaiting Pi confirmation"
|
|
9783
|
+
});
|
|
9478
9784
|
}
|
|
9479
9785
|
stagePendingCompaction(stated, buildSuccessMetrics(stated, "success"));
|
|
9480
9786
|
if (stated.cancellation.timedOut) {
|
|
@@ -9486,6 +9792,7 @@ async function runSmartCompact(opts) {
|
|
|
9486
9792
|
keepApplyProgress = true;
|
|
9487
9793
|
}
|
|
9488
9794
|
} catch (err) {
|
|
9795
|
+
runFailed = true;
|
|
9489
9796
|
recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
9490
9797
|
throw err;
|
|
9491
9798
|
} finally {
|
|
@@ -9502,7 +9809,8 @@ async function runSmartCompact(opts) {
|
|
|
9502
9809
|
if (base.flags.autoTriggered && !base.cancellation.timedOut) {
|
|
9503
9810
|
const dur = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
|
|
9504
9811
|
const hasPending = base.pendingRef.isPresent(runSessionId);
|
|
9505
|
-
|
|
9812
|
+
if (hasPending || runFailed || finalRc)
|
|
9813
|
+
base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : runFailed ? "Smart compact stopped safely in " + dur + " \xB7 no summary applied \xB7 Pi fallback continues" : "Smart compact run finished in " + dur, runFailed ? "warning" : "info");
|
|
9506
9814
|
}
|
|
9507
9815
|
}
|
|
9508
9816
|
}
|
|
@@ -9646,8 +9954,8 @@ function createCompactionCommitStore(options = {}) {
|
|
|
9646
9954
|
|
|
9647
9955
|
// src/app/native-continuity-bridge.ts
|
|
9648
9956
|
import crypto6 from "crypto";
|
|
9649
|
-
import
|
|
9650
|
-
import
|
|
9957
|
+
import fs9 from "fs";
|
|
9958
|
+
import path12 from "path";
|
|
9651
9959
|
var MAX_TEXT_BYTES = 256 * 1024;
|
|
9652
9960
|
function sameScope(a, b) {
|
|
9653
9961
|
return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
|
|
@@ -9657,14 +9965,14 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9657
9965
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
9658
9966
|
const now = opts.now ?? Date.now;
|
|
9659
9967
|
const dir = opts.dir ?? nativeContinuityDir();
|
|
9660
|
-
const lockTarget =
|
|
9661
|
-
const fileFor = (scope) =>
|
|
9968
|
+
const lockTarget = path12.join(dir, "bridge");
|
|
9969
|
+
const fileFor = (scope) => path12.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
|
|
9662
9970
|
const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
|
|
9663
9971
|
const readEntry = (file) => {
|
|
9664
9972
|
try {
|
|
9665
|
-
if (
|
|
9973
|
+
if (fs9.statSync(file).size > MAX_TEXT_BYTES * 2)
|
|
9666
9974
|
return null;
|
|
9667
|
-
const value = JSON.parse(
|
|
9975
|
+
const value = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
9668
9976
|
if (value.schemaVersion !== 1 || typeof value.text !== "string" || Buffer.byteLength(value.text) > MAX_TEXT_BYTES || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt) || !value.scope || !validScope(value.scope))
|
|
9669
9977
|
return null;
|
|
9670
9978
|
return value;
|
|
@@ -9676,16 +9984,16 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9676
9984
|
const fresh = [];
|
|
9677
9985
|
let files = [];
|
|
9678
9986
|
try {
|
|
9679
|
-
files =
|
|
9987
|
+
files = fs9.readdirSync(dir).filter((file) => file.endsWith(".json"));
|
|
9680
9988
|
} catch {
|
|
9681
9989
|
return fresh;
|
|
9682
9990
|
}
|
|
9683
9991
|
for (const name of files) {
|
|
9684
|
-
const file =
|
|
9992
|
+
const file = path12.join(dir, name);
|
|
9685
9993
|
const entry = readEntry(file);
|
|
9686
9994
|
if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
|
|
9687
9995
|
try {
|
|
9688
|
-
|
|
9996
|
+
fs9.unlinkSync(file);
|
|
9689
9997
|
} catch {}
|
|
9690
9998
|
} else {
|
|
9691
9999
|
fresh.push({ file, entry });
|
|
@@ -9696,7 +10004,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9696
10004
|
const oldest = fresh.shift();
|
|
9697
10005
|
if (oldest)
|
|
9698
10006
|
try {
|
|
9699
|
-
|
|
10007
|
+
fs9.unlinkSync(oldest.file);
|
|
9700
10008
|
} catch {}
|
|
9701
10009
|
}
|
|
9702
10010
|
return fresh;
|
|
@@ -9704,7 +10012,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9704
10012
|
const locked = (work) => {
|
|
9705
10013
|
ensureDir(dir);
|
|
9706
10014
|
try {
|
|
9707
|
-
|
|
10015
|
+
fs9.chmodSync(dir, 448);
|
|
9708
10016
|
} catch {}
|
|
9709
10017
|
const release = acquireLockSync(lockTarget);
|
|
9710
10018
|
try {
|
|
@@ -9721,13 +10029,13 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9721
10029
|
locked(() => {
|
|
9722
10030
|
const target = fileFor(scope);
|
|
9723
10031
|
try {
|
|
9724
|
-
|
|
10032
|
+
fs9.unlinkSync(target);
|
|
9725
10033
|
} catch {}
|
|
9726
10034
|
prune(1);
|
|
9727
10035
|
const entry = { schemaVersion: 1, scope, text, createdAt: now() };
|
|
9728
10036
|
atomicWriteFileSync(target, JSON.stringify(entry));
|
|
9729
10037
|
try {
|
|
9730
|
-
|
|
10038
|
+
fs9.chmodSync(target, 384);
|
|
9731
10039
|
} catch {}
|
|
9732
10040
|
});
|
|
9733
10041
|
} catch (error2) {
|
|
@@ -9745,7 +10053,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9745
10053
|
if (!entry)
|
|
9746
10054
|
return null;
|
|
9747
10055
|
try {
|
|
9748
|
-
|
|
10056
|
+
fs9.unlinkSync(target);
|
|
9749
10057
|
} catch {
|
|
9750
10058
|
return null;
|
|
9751
10059
|
}
|
|
@@ -9761,13 +10069,13 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9761
10069
|
locked(() => {
|
|
9762
10070
|
if (scope) {
|
|
9763
10071
|
try {
|
|
9764
|
-
|
|
10072
|
+
fs9.unlinkSync(fileFor(scope));
|
|
9765
10073
|
} catch {}
|
|
9766
10074
|
return;
|
|
9767
10075
|
}
|
|
9768
10076
|
for (const item of prune(0)) {
|
|
9769
10077
|
try {
|
|
9770
|
-
|
|
10078
|
+
fs9.unlinkSync(item.file);
|
|
9771
10079
|
} catch {}
|
|
9772
10080
|
}
|
|
9773
10081
|
});
|
|
@@ -9828,15 +10136,16 @@ function resolveModels(ctx, primary, config, explicit = false) {
|
|
|
9828
10136
|
return { segModel, sumModel, verifyModel };
|
|
9829
10137
|
}
|
|
9830
10138
|
function resolveGraphScope(ctx) {
|
|
10139
|
+
const projectId = deriveProjectIdFromCwd(ctx.cwd);
|
|
9831
10140
|
const sessionId = resolveSessionId(ctx);
|
|
9832
|
-
if (isUnresolvedSessionId(sessionId))
|
|
10141
|
+
if (!projectId || isUnresolvedSessionId(sessionId))
|
|
9833
10142
|
return null;
|
|
9834
|
-
const
|
|
10143
|
+
const ancestryIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
9835
10144
|
return {
|
|
9836
|
-
projectId
|
|
10145
|
+
projectId,
|
|
9837
10146
|
sessionId,
|
|
9838
|
-
branchHeadId:
|
|
9839
|
-
branchEntryIds
|
|
10147
|
+
branchHeadId: ancestryIds.at(-1),
|
|
10148
|
+
branchEntryIds: ancestryIds
|
|
9840
10149
|
};
|
|
9841
10150
|
}
|
|
9842
10151
|
function smartCompactExtension(pi) {
|
|
@@ -9918,7 +10227,7 @@ function smartCompactExtension(pi) {
|
|
|
9918
10227
|
}
|
|
9919
10228
|
const scope = resolveGraphScope(ctx);
|
|
9920
10229
|
if (!scope)
|
|
9921
|
-
return { content: [{ type: "text", text: "Smart Recall needs a persisted session id." }], details: undefined };
|
|
10230
|
+
return { content: [{ type: "text", text: "Smart Recall must run from a project directory and needs a persisted session id." }], details: undefined };
|
|
9922
10231
|
const results = recallContext(scope, params.query, {
|
|
9923
10232
|
limit: params.limit,
|
|
9924
10233
|
sessionOnly: params.scope === "session",
|
|
@@ -9952,11 +10261,11 @@ function smartCompactExtension(pi) {
|
|
|
9952
10261
|
}
|
|
9953
10262
|
const scope = resolveGraphScope(ctx);
|
|
9954
10263
|
if (!scope)
|
|
9955
|
-
return { content: [{ type: "text", text: "Saving memory needs a persisted session id." }], details: undefined };
|
|
10264
|
+
return { content: [{ type: "text", text: "Saving project memory must run from a project directory and needs a persisted session id." }], details: undefined };
|
|
9956
10265
|
const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
|
|
9957
10266
|
const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
|
|
9958
10267
|
const content = scrubber.scrubText(params.content).value;
|
|
9959
|
-
const relatedPaths = (params.related_paths ?? []).map((
|
|
10268
|
+
const relatedPaths = (params.related_paths ?? []).map((path13) => scrubber.scrubText(path13).value);
|
|
9960
10269
|
const status = params.status ?? "active";
|
|
9961
10270
|
if (!ctx.hasUI) {
|
|
9962
10271
|
return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
|
|
@@ -9964,7 +10273,7 @@ function smartCompactExtension(pi) {
|
|
|
9964
10273
|
const approved = await ctx.ui.confirm(status === "resolved" ? "Resolve Project Memory" : "Save Project Memory", "Kind: " + params.kind + `
|
|
9965
10274
|
Title: ` + title + `
|
|
9966
10275
|
|
|
9967
|
-
` + content
|
|
10276
|
+
` + content + (relatedPaths.length ? `
|
|
9968
10277
|
|
|
9969
10278
|
Paths: ` + relatedPaths.join(", ") : ""));
|
|
9970
10279
|
if (!approved || signal?.aborted) {
|
|
@@ -9987,7 +10296,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
9987
10296
|
pi.registerCommand("smart-compact", {
|
|
9988
10297
|
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [mode] [flags] [--focus=topic] [--max-calls=N] [--max-input-tokens=N] [note]",
|
|
9989
10298
|
getArgumentCompletions: (prefix) => {
|
|
9990
|
-
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "restore", "loops", "
|
|
10299
|
+
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "restore", "loops", "fast", "balanced", "thorough", "--focus=", "--max-calls=", "--max-input-tokens=", "--max-latency="].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
9991
10300
|
return m.length ? m : null;
|
|
9992
10301
|
},
|
|
9993
10302
|
handler: async (args, ctx) => {
|
|
@@ -10005,16 +10314,16 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10005
10314
|
const maxLlmCalls = maxCallsRaw == null ? undefined : Number(maxCallsRaw);
|
|
10006
10315
|
const maxLlmInputTokens = maxInputRaw == null ? undefined : Number(maxInputRaw);
|
|
10007
10316
|
const maxLatencyMs = maxLatencyRaw == null ? undefined : Number(maxLatencyRaw);
|
|
10008
|
-
if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls <
|
|
10009
|
-
ctx.ui.notify("--max-calls must be an integer from
|
|
10317
|
+
if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls < BUDGET_LIMITS.CALLS.min || maxLlmCalls > BUDGET_LIMITS.CALLS.max)) {
|
|
10318
|
+
ctx.ui.notify("--max-calls must be an integer from " + BUDGET_LIMITS.CALLS.min + " to " + BUDGET_LIMITS.CALLS.max, "error");
|
|
10010
10319
|
return;
|
|
10011
10320
|
}
|
|
10012
|
-
if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens <
|
|
10013
|
-
ctx.ui.notify("--max-input-tokens must be an integer from
|
|
10321
|
+
if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens < BUDGET_LIMITS.INPUT_TOKENS.min || maxLlmInputTokens > BUDGET_LIMITS.INPUT_TOKENS.max)) {
|
|
10322
|
+
ctx.ui.notify("--max-input-tokens must be an integer from " + BUDGET_LIMITS.INPUT_TOKENS.min + " to " + BUDGET_LIMITS.INPUT_TOKENS.max, "error");
|
|
10014
10323
|
return;
|
|
10015
10324
|
}
|
|
10016
|
-
if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs <
|
|
10017
|
-
ctx.ui.notify("--max-latency must be
|
|
10325
|
+
if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs < BUDGET_LIMITS.LATENCY_MS.min || maxLatencyMs > BUDGET_LIMITS.LATENCY_MS.max)) {
|
|
10326
|
+
ctx.ui.notify("--max-latency must be " + BUDGET_LIMITS.LATENCY_MS.min + "\u2013" + BUDGET_LIMITS.LATENCY_MS.max + " ms", "error");
|
|
10018
10327
|
return;
|
|
10019
10328
|
}
|
|
10020
10329
|
if (flags.includes("metrics") || flags.includes("dashboard")) {
|
|
@@ -10082,8 +10391,12 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10082
10391
|
}
|
|
10083
10392
|
if (flags.includes("loops")) {
|
|
10084
10393
|
const projectId = deriveProjectIdFromCwd(ctx.cwd);
|
|
10394
|
+
if (!projectId) {
|
|
10395
|
+
ctx.ui.notify("Project loops must be managed from a project directory", "warning");
|
|
10396
|
+
return;
|
|
10397
|
+
}
|
|
10085
10398
|
const sessionId = resolveSessionId(ctx);
|
|
10086
|
-
const branchIds = ctx.sessionManager.getBranch()
|
|
10399
|
+
const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
10087
10400
|
const state = isUnresolvedSessionId(sessionId) ? null : loadScopedCompactionState({ projectId, sessionId }, branchIds);
|
|
10088
10401
|
if (!state || state.openLoops.length === 0) {
|
|
10089
10402
|
ctx.ui.notify("No persisted open loops for this project", "info");
|
|
@@ -10104,8 +10417,10 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10104
10417
|
const knownProviders = new Set(ctx.modelRegistry.getAvailable().map((m) => m.provider));
|
|
10105
10418
|
const modelArg = tokens.find((t) => /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t) && (findModelById(ctx, t) || knownProviders.has(t.split("/")[0])));
|
|
10106
10419
|
const config = loadConfig();
|
|
10107
|
-
const rawMode = tokens.find((t) => ["auto", "
|
|
10108
|
-
const modeArg = rawMode === "slow" ? "thorough" : rawMode;
|
|
10420
|
+
const rawMode = tokens.find((t) => ["auto", "fast", "balanced", "thorough", "aggressive", "slow"].includes(t));
|
|
10421
|
+
const modeArg = rawMode === "slow" ? "thorough" : rawMode === "aggressive" ? "fast" : rawMode;
|
|
10422
|
+
if (rawMode === "aggressive")
|
|
10423
|
+
ctx.ui.notify("Aggressive mode is now Fast; using Fast.", "warning");
|
|
10109
10424
|
const profileArg = tokens.includes("light") ? "light" : undefined;
|
|
10110
10425
|
const mode = modeArg ?? (profileArg ? modeFromLegacyProfile(profileArg) : config.mode);
|
|
10111
10426
|
if (!tokens.length) {
|
|
@@ -10222,7 +10537,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10222
10537
|
cancellationOut
|
|
10223
10538
|
});
|
|
10224
10539
|
} catch (err) {
|
|
10225
|
-
|
|
10540
|
+
debugError("Smart compact auto-trigger stopped", err);
|
|
10226
10541
|
} finally {
|
|
10227
10542
|
clearTimeout(timeoutId);
|
|
10228
10543
|
}
|
|
@@ -10232,7 +10547,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10232
10547
|
}
|
|
10233
10548
|
}
|
|
10234
10549
|
} catch (e) {
|
|
10235
|
-
|
|
10550
|
+
debugError("session_before_compact stopped", e);
|
|
10236
10551
|
}
|
|
10237
10552
|
});
|
|
10238
10553
|
pi.on("session_compact", async (event, ctx) => {
|
|
@@ -10262,7 +10577,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10262
10577
|
if (isUnresolvedSessionId(sessionId))
|
|
10263
10578
|
return;
|
|
10264
10579
|
const projectId = deriveProjectIdFromCwd(ctx.cwd);
|
|
10265
|
-
|
|
10580
|
+
if (!projectId)
|
|
10581
|
+
return;
|
|
10582
|
+
const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
10266
10583
|
const branchHeadId = typeof event.compactionEntry.id === "string" ? event.compactionEntry.id : branchIds.at(-1);
|
|
10267
10584
|
if (!branchHeadId)
|
|
10268
10585
|
return;
|
|
@@ -10331,27 +10648,27 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10331
10648
|
parameters: {
|
|
10332
10649
|
type: "object",
|
|
10333
10650
|
properties: {
|
|
10334
|
-
mode: { type: "string", description: "
|
|
10651
|
+
mode: { type: "string", description: "fast, balanced, thorough, or auto. Default: auto." },
|
|
10335
10652
|
profile: { type: "string", description: "Deprecated alias: light, balanced, or aggressive." },
|
|
10336
10653
|
verbose: { type: "boolean", description: "Show detailed pipeline output." },
|
|
10337
10654
|
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." },
|
|
10338
10655
|
report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
|
|
10339
10656
|
dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." },
|
|
10340
10657
|
focus: { type: "string", description: "Topic or path that should receive extra preservation budget." },
|
|
10341
|
-
max_calls: { type: "number", description: "Maximum LLM calls for this run (
|
|
10342
|
-
max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (
|
|
10343
|
-
max_latency_ms: { type: "number", description: "Optional
|
|
10658
|
+
max_calls: { type: "number", description: "Maximum LLM calls for this run (" + BUDGET_LIMITS.CALLS.min + "-" + BUDGET_LIMITS.CALLS.max + ")." },
|
|
10659
|
+
max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (" + BUDGET_LIMITS.INPUT_TOKENS.min + "-" + BUDGET_LIMITS.INPUT_TOKENS.max + ")." },
|
|
10660
|
+
max_latency_ms: { type: "number", description: "Optional pipeline cancellation budget in milliseconds (" + BUDGET_LIMITS.LATENCY_MS.min + "-" + BUDGET_LIMITS.LATENCY_MS.max + ")." }
|
|
10344
10661
|
}
|
|
10345
10662
|
},
|
|
10346
10663
|
async execute(_id, params, signal, _onUp, ctx) {
|
|
10347
10664
|
const profile = params.profile === "light" || params.profile === "balanced" || params.profile === "aggressive" ? params.profile : undefined;
|
|
10348
|
-
const mode = params.mode === "slow" ? "thorough" :
|
|
10665
|
+
const mode = params.mode === "slow" ? "thorough" : params.mode === "aggressive" ? "fast" : ["auto", "fast", "balanced", "thorough"].find((value) => value === params.mode);
|
|
10349
10666
|
const verbose = !!params.verbose;
|
|
10350
10667
|
const dryRun = !!params.dry_run;
|
|
10351
10668
|
const focus = typeof params.focus === "string" ? params.focus.trim() || undefined : undefined;
|
|
10352
|
-
const maxLlmCalls = typeof params.max_calls === "number" && Number.isInteger(params.max_calls) && params.max_calls >=
|
|
10353
|
-
const maxLlmInputTokens = typeof params.max_input_tokens === "number" && Number.isInteger(params.max_input_tokens) && params.max_input_tokens >=
|
|
10354
|
-
const maxLatencyMs = typeof params.max_latency_ms === "number" && params.max_latency_ms >=
|
|
10669
|
+
const maxLlmCalls = typeof params.max_calls === "number" && Number.isInteger(params.max_calls) && params.max_calls >= BUDGET_LIMITS.CALLS.min && params.max_calls <= BUDGET_LIMITS.CALLS.max ? params.max_calls : undefined;
|
|
10670
|
+
const maxLlmInputTokens = typeof params.max_input_tokens === "number" && Number.isInteger(params.max_input_tokens) && params.max_input_tokens >= BUDGET_LIMITS.INPUT_TOKENS.min && params.max_input_tokens <= BUDGET_LIMITS.INPUT_TOKENS.max ? params.max_input_tokens : undefined;
|
|
10671
|
+
const maxLatencyMs = typeof params.max_latency_ms === "number" && params.max_latency_ms >= BUDGET_LIMITS.LATENCY_MS.min && params.max_latency_ms <= BUDGET_LIMITS.LATENCY_MS.max ? params.max_latency_ms : undefined;
|
|
10355
10672
|
if (params.report || params.dashboard) {
|
|
10356
10673
|
const report = buildMetricsReport();
|
|
10357
10674
|
const fp = params.dashboard ? writeMetricsDashboard() : null;
|