pi-smart-compact 8.0.6 → 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 +19 -0
- package/README.md +70 -51
- 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/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/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 +759 -548
- 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 +150 -110
- package/dist/telemetry-report.js +138 -41
- package/dist/ui/dashboard-insights.d.ts.map +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/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 +30 -13
- 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
|
@@ -17,7 +17,6 @@ var MODE_POLICIES = {
|
|
|
17
17
|
allowLlmPatch: false,
|
|
18
18
|
singlePassMultiplier: 2,
|
|
19
19
|
batchOutput: { min: 800, perChunk: 160, max: 2400 },
|
|
20
|
-
softLatencyMs: 30000,
|
|
21
20
|
targetContextPercent: 30
|
|
22
21
|
},
|
|
23
22
|
balanced: {
|
|
@@ -29,7 +28,6 @@ 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
33
|
thorough: {
|
|
@@ -41,7 +39,6 @@ var MODE_POLICIES = {
|
|
|
41
39
|
allowLlmPatch: true,
|
|
42
40
|
singlePassMultiplier: 0.9,
|
|
43
41
|
batchOutput: { min: 1500, perChunk: 400, max: 6000 },
|
|
44
|
-
softLatencyMs: 120000,
|
|
45
42
|
targetContextPercent: 50
|
|
46
43
|
}
|
|
47
44
|
};
|
|
@@ -105,10 +102,16 @@ function effectiveBudget(configured, modeDefault) {
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
// src/constants.ts
|
|
108
|
-
var VERSION = "8.0.
|
|
105
|
+
var VERSION = "8.0.7";
|
|
109
106
|
var CHARS_PER_TOKEN = 3.8;
|
|
110
107
|
var MIN_COMPACTION_SAVING_RATIO = 0.1;
|
|
111
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
|
+
};
|
|
112
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.";
|
|
113
116
|
var PROFILES = {
|
|
114
117
|
light: {
|
|
@@ -480,13 +483,8 @@ var LOCK_STALE_MS = 5000;
|
|
|
480
483
|
var LOCK_RETRY_MS = 25;
|
|
481
484
|
var LOCK_MAX_RETRIES = 80;
|
|
482
485
|
function ensureDir(dir) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
} catch (e) {
|
|
486
|
-
if (e?.code !== "EEXIST") {
|
|
487
|
-
warn("ensureDir failed for " + dir, e);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
486
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
487
|
+
fs.chmodSync(dir, 448);
|
|
490
488
|
}
|
|
491
489
|
function tempPath(target) {
|
|
492
490
|
return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
@@ -495,8 +493,9 @@ function atomicWriteFileSync(target, data) {
|
|
|
495
493
|
ensureDir(path2.dirname(target));
|
|
496
494
|
const tmp = tempPath(target);
|
|
497
495
|
try {
|
|
498
|
-
fs.writeFileSync(tmp, data);
|
|
496
|
+
fs.writeFileSync(tmp, data, { mode: 384 });
|
|
499
497
|
fs.renameSync(tmp, target);
|
|
498
|
+
fs.chmodSync(target, 384);
|
|
500
499
|
} catch (e) {
|
|
501
500
|
try {
|
|
502
501
|
fs.unlinkSync(tmp);
|
|
@@ -508,7 +507,7 @@ function acquireLockSync(target) {
|
|
|
508
507
|
const lockDir = target + ".lock";
|
|
509
508
|
for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
510
509
|
try {
|
|
511
|
-
fs.mkdirSync(lockDir);
|
|
510
|
+
fs.mkdirSync(lockDir, { mode: 448 });
|
|
512
511
|
return () => {
|
|
513
512
|
try {
|
|
514
513
|
fs.rmdirSync(lockDir);
|
|
@@ -550,9 +549,12 @@ function appendLineLocked(target, line) {
|
|
|
550
549
|
ensureDir(path2.dirname(target));
|
|
551
550
|
const release = acquireLockSync(target);
|
|
552
551
|
try {
|
|
552
|
+
if (fs.existsSync(target))
|
|
553
|
+
fs.chmodSync(target, 384);
|
|
553
554
|
fs.appendFileSync(target, line.endsWith(`
|
|
554
555
|
`) ? line : line + `
|
|
555
|
-
|
|
556
|
+
`, { mode: 384 });
|
|
557
|
+
fs.chmodSync(target, 384);
|
|
556
558
|
} finally {
|
|
557
559
|
release();
|
|
558
560
|
}
|
|
@@ -641,6 +643,27 @@ function writeJsonSync(target, value, pretty = false) {
|
|
|
641
643
|
// src/utils/extraction.ts
|
|
642
644
|
import path3 from "path";
|
|
643
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
|
+
|
|
644
667
|
// src/utils/tokens.ts
|
|
645
668
|
var PROVIDER_MAP = {
|
|
646
669
|
"zai-anthropic": {
|
|
@@ -838,35 +861,19 @@ class TokenCalibrationStore {
|
|
|
838
861
|
if (!provider)
|
|
839
862
|
return 1;
|
|
840
863
|
const exactKey = calibrationKey(provider, model);
|
|
841
|
-
const exact = this.factors
|
|
842
|
-
if (exact !== undefined)
|
|
843
|
-
this.factors.delete(exactKey);
|
|
844
|
-
this.factors.set(exactKey, exact);
|
|
864
|
+
const exact = lruGet(this.factors, exactKey);
|
|
865
|
+
if (exact !== undefined)
|
|
845
866
|
return exact;
|
|
846
|
-
|
|
847
|
-
const providerKey = calibrationKey(provider);
|
|
848
|
-
const fallback = this.factors.get(providerKey);
|
|
849
|
-
if (fallback !== undefined) {
|
|
850
|
-
this.factors.delete(providerKey);
|
|
851
|
-
this.factors.set(providerKey, fallback);
|
|
852
|
-
}
|
|
853
|
-
return fallback ?? 1;
|
|
867
|
+
return lruGet(this.factors, calibrationKey(provider)) ?? 1;
|
|
854
868
|
}
|
|
855
869
|
calibrate(estimated, actual, provider, model) {
|
|
856
870
|
if (actual <= 0 || estimated <= 0 || !provider)
|
|
857
871
|
return;
|
|
858
872
|
const key = calibrationKey(provider, model);
|
|
859
|
-
const prev = this.factors
|
|
873
|
+
const prev = lruGet(this.factors, key) ?? 1;
|
|
860
874
|
const target = prev * actual / estimated;
|
|
861
875
|
const clamped = Math.max(TUNING.CALIBRATION_CLAMP_MIN, Math.min(TUNING.CALIBRATION_CLAMP_MAX, target));
|
|
862
|
-
this.factors.
|
|
863
|
-
this.factors.set(key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE);
|
|
864
|
-
while (this.factors.size > Math.max(1, this.maxEntries)) {
|
|
865
|
-
const oldest = this.factors.keys().next().value;
|
|
866
|
-
if (oldest === undefined)
|
|
867
|
-
break;
|
|
868
|
-
this.factors.delete(oldest);
|
|
869
|
-
}
|
|
876
|
+
lruSet(this.factors, key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE, Math.max(1, this.maxEntries));
|
|
870
877
|
}
|
|
871
878
|
size() {
|
|
872
879
|
return this.factors.size;
|
|
@@ -1206,12 +1213,192 @@ function extractFileRefs(summary) {
|
|
|
1206
1213
|
return candidates.filter(isLikelyFileRef);
|
|
1207
1214
|
}
|
|
1208
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
|
+
|
|
1209
1394
|
// src/utils/extraction.ts
|
|
1210
1395
|
var TRUNCATE_RE = /\u2026\u2702\d+$/;
|
|
1211
1396
|
function isTruncated(content) {
|
|
1212
1397
|
return TRUNCATE_RE.test(extractText(content));
|
|
1213
1398
|
}
|
|
1214
|
-
|
|
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
|
+
}
|
|
1215
1402
|
function flattenToolCallBlock(b) {
|
|
1216
1403
|
if (!isToolCallBlock(b))
|
|
1217
1404
|
return [];
|
|
@@ -1292,8 +1479,8 @@ function buildToolCallIndex(msgs) {
|
|
|
1292
1479
|
const nested = flattenToolCallBlock(b);
|
|
1293
1480
|
for (let t = 0;t < nested.length; t++) {
|
|
1294
1481
|
const tool = nested[t];
|
|
1295
|
-
const
|
|
1296
|
-
idx.set(
|
|
1482
|
+
const id = nestedToolCallId(b.id, i, t, tool.id);
|
|
1483
|
+
idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
|
|
1297
1484
|
}
|
|
1298
1485
|
}
|
|
1299
1486
|
}
|
|
@@ -1321,14 +1508,15 @@ function trackFileOps(msgs, _tcIdx) {
|
|
|
1321
1508
|
if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
|
|
1322
1509
|
const existing = modMap.get(filePath);
|
|
1323
1510
|
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1511
|
+
delSet.delete(filePath);
|
|
1324
1512
|
}
|
|
1325
1513
|
} else if (operation === "delete") {
|
|
1326
1514
|
delSet.add(filePath);
|
|
1515
|
+
modMap.delete(filePath);
|
|
1516
|
+
readSet.delete(filePath);
|
|
1327
1517
|
} else if (operation === "read" || operation === "search" || operation === "list") {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
else
|
|
1331
|
-
readSet.add(filePath);
|
|
1518
|
+
readSet.add(filePath);
|
|
1519
|
+
delSet.delete(filePath);
|
|
1332
1520
|
}
|
|
1333
1521
|
}
|
|
1334
1522
|
return {
|
|
@@ -1354,6 +1542,10 @@ function hasCommandFailureSignal(text) {
|
|
|
1354
1542
|
const firstLine = text.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
1355
1543
|
return LIKELY_ERROR_RE.test(firstLine) || /^(?:npm\s+error|fatal:|traceback\b)/i.test(firstLine);
|
|
1356
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
|
+
}
|
|
1357
1549
|
function catalogErrors(msgs, _tcIdx) {
|
|
1358
1550
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
1359
1551
|
const errors = [];
|
|
@@ -1363,7 +1555,7 @@ function catalogErrors(msgs, _tcIdx) {
|
|
|
1363
1555
|
continue;
|
|
1364
1556
|
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
1365
1557
|
const text = extractText(m.content);
|
|
1366
|
-
if (tc && isBenignSearchResult(tc, text))
|
|
1558
|
+
if (tc && isBenignSearchResult(tc, text) || isTransientToolDiagnostic(text))
|
|
1367
1559
|
continue;
|
|
1368
1560
|
if (m.isError) {
|
|
1369
1561
|
errors.push({ index: i, tool: tc?.name ?? "unknown", message: text.slice(0, TRUNC.ERROR_DETAIL), retryAttempted: false, resolved: false });
|
|
@@ -1550,13 +1742,27 @@ function buildTimeline(msgs, errors) {
|
|
|
1550
1742
|
...timeline.filter((t) => t.event === "error")
|
|
1551
1743
|
].sort((a, b) => a.index - b.index) : timeline;
|
|
1552
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
|
+
}
|
|
1553
1751
|
function extractMainGoal(msgs) {
|
|
1554
|
-
for (
|
|
1752
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
1753
|
+
const m = msgs[i];
|
|
1555
1754
|
if (m?.role !== "user")
|
|
1556
1755
|
continue;
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
1559
|
-
|
|
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;
|
|
1560
1766
|
}
|
|
1561
1767
|
return null;
|
|
1562
1768
|
}
|
|
@@ -3078,12 +3284,22 @@ function mergeExtractions(base, delta, baseMsgCount, deltaMessages = [], deltaTo
|
|
|
3078
3284
|
const previous = modified.get(file.path);
|
|
3079
3285
|
modified.set(file.path, previous ? { ...file, toolCalls: previous.toolCalls + file.toolCalls, lastModifiedIndex: Math.max(previous.lastModifiedIndex, file.lastModifiedIndex) } : file);
|
|
3080
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);
|
|
3081
3297
|
const reconciledBaseErrors = reconcileCachedErrors(base.errors, deltaMessages, deltaToolCalls, baseMsgCount);
|
|
3082
|
-
const mergedErrors = [...reconciledBaseErrors, ...offsetErrors];
|
|
3298
|
+
const mergedErrors = [...reconciledBaseErrors, ...offsetErrors].filter((error2) => !isTransientToolDiagnostic(error2.message));
|
|
3083
3299
|
return {
|
|
3084
3300
|
modifiedFiles: [...modified.values()],
|
|
3085
|
-
readFiles: [...
|
|
3086
|
-
deletedFiles: [...
|
|
3301
|
+
readFiles: [...readFiles],
|
|
3302
|
+
deletedFiles: [...deletedFiles],
|
|
3087
3303
|
referencedFiles: [...new Set([...base.referencedFiles ?? [], ...delta.referencedFiles ?? []])].slice(0, 200),
|
|
3088
3304
|
mediaAttachments: [...base.mediaAttachments ?? [], ...offsetMedia],
|
|
3089
3305
|
errors: mergedErrors,
|
|
@@ -3091,7 +3307,7 @@ function mergeExtractions(base, delta, baseMsgCount, deltaMessages = [], deltaTo
|
|
|
3091
3307
|
constraints: [...base.constraints, ...offsetConstraints],
|
|
3092
3308
|
topics: [...base.topics, ...offsetTopics],
|
|
3093
3309
|
timeline: [...base.timeline, ...offsetTimeline],
|
|
3094
|
-
mainGoal:
|
|
3310
|
+
mainGoal: delta.mainGoal ?? base.mainGoal,
|
|
3095
3311
|
lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
|
|
3096
3312
|
lastErrors: mergedErrors.filter((error2) => !error2.resolved).map((error2) => error2.message).slice(-3),
|
|
3097
3313
|
messageCount: baseMsgCount + delta.messageCount
|
|
@@ -3212,10 +3428,10 @@ function p95(values) {
|
|
|
3212
3428
|
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
|
|
3213
3429
|
}
|
|
3214
3430
|
function stats(entries, damage) {
|
|
3215
|
-
const
|
|
3216
|
-
const
|
|
3217
|
-
const
|
|
3218
|
-
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));
|
|
3219
3435
|
const observedScores = new Map;
|
|
3220
3436
|
for (const observation of damage) {
|
|
3221
3437
|
if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
|
|
@@ -3225,14 +3441,15 @@ function stats(entries, damage) {
|
|
|
3225
3441
|
const damaging = [...observedScores.values()].filter((score) => score > 0).length;
|
|
3226
3442
|
return {
|
|
3227
3443
|
runs: entries.length,
|
|
3228
|
-
|
|
3444
|
+
appliedRuns: evidence.length,
|
|
3445
|
+
successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
|
|
3229
3446
|
avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
|
|
3230
|
-
qualityCoverage:
|
|
3231
|
-
p95LatencyMs: p95(
|
|
3232
|
-
avgTokens:
|
|
3233
|
-
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,
|
|
3234
3451
|
damageRate: observedScores.size ? damaging / observedScores.size : 0,
|
|
3235
|
-
damageCoverage:
|
|
3452
|
+
damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
|
|
3236
3453
|
};
|
|
3237
3454
|
}
|
|
3238
3455
|
function roundStats(value) {
|
|
@@ -3257,7 +3474,7 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3257
3474
|
const triggers = [];
|
|
3258
3475
|
const failureBaseline = 1 - baseline.successRate;
|
|
3259
3476
|
const failureCanary = 1 - canary.successRate;
|
|
3260
|
-
if (canary.
|
|
3477
|
+
if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
|
|
3261
3478
|
triggers.push({
|
|
3262
3479
|
metric: "failure-rate",
|
|
3263
3480
|
baseline: failureBaseline,
|
|
@@ -3285,15 +3502,17 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3285
3502
|
if (canary.damageRate - baseline.damageRate >= 0.1) {
|
|
3286
3503
|
triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
|
|
3287
3504
|
}
|
|
3288
|
-
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));
|
|
3289
3508
|
const reasons = [];
|
|
3290
3509
|
let decision = "hold";
|
|
3291
|
-
if (triggers.length && canary.
|
|
3510
|
+
if (triggers.length && canary.appliedRuns >= 3) {
|
|
3292
3511
|
decision = "rollback";
|
|
3293
3512
|
reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
|
|
3294
|
-
} else if (canary.
|
|
3295
|
-
reasons.push("need " + (minCanaryRuns - canary.
|
|
3296
|
-
} 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)) {
|
|
3297
3516
|
reasons.push("stable baseline is too small");
|
|
3298
3517
|
} else if (canary.qualityCoverage < 0.7) {
|
|
3299
3518
|
reasons.push("schema-v2 quality coverage is below 70%");
|
|
@@ -3319,7 +3538,7 @@ function assessCanary(entries, damageEntries, options) {
|
|
|
3319
3538
|
reasons
|
|
3320
3539
|
};
|
|
3321
3540
|
}
|
|
3322
|
-
var
|
|
3541
|
+
var TELEMETRY_FAILURE_KINDS = new Set([
|
|
3323
3542
|
"cancelled",
|
|
3324
3543
|
"timeout",
|
|
3325
3544
|
"rate-limit",
|
|
@@ -3333,6 +3552,9 @@ var FAILURE_KINDS = new Set([
|
|
|
3333
3552
|
"yield",
|
|
3334
3553
|
"internal"
|
|
3335
3554
|
]);
|
|
3555
|
+
function isTelemetryFailureKind(value) {
|
|
3556
|
+
return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
|
|
3557
|
+
}
|
|
3336
3558
|
|
|
3337
3559
|
// src/ui/dashboard-format.ts
|
|
3338
3560
|
var DASHBOARD_PAGE_SIZE = 24;
|
|
@@ -3628,7 +3850,7 @@ function formatDashboardCanary(insights) {
|
|
|
3628
3850
|
"Canary / stable control",
|
|
3629
3851
|
"",
|
|
3630
3852
|
"Decision: " + c.decision.toUpperCase() + " | data confidence " + c.dataConfidence + "%",
|
|
3631
|
-
"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,
|
|
3632
3854
|
"Success: stable " + Math.round(c.baseline.successRate * 100) + "% | canary " + Math.round(c.canary.successRate * 100) + "%",
|
|
3633
3855
|
"Quality: stable " + (c.baseline.avgQuality?.toFixed(1) ?? "\u2014") + " | canary " + (c.canary.avgQuality?.toFixed(1) ?? "\u2014"),
|
|
3634
3856
|
"p95: stable " + c.baseline.p95LatencyMs + "ms | canary " + c.canary.p95LatencyMs + "ms",
|
|
@@ -3643,21 +3865,8 @@ function formatDashboardCanary(insights) {
|
|
|
3643
3865
|
}
|
|
3644
3866
|
function buildDashboardInsights(entries, damageEntries = [], options = {}) {
|
|
3645
3867
|
const failures = {};
|
|
3646
|
-
const knownFailures = new Set([
|
|
3647
|
-
"cancelled",
|
|
3648
|
-
"timeout",
|
|
3649
|
-
"rate-limit",
|
|
3650
|
-
"authentication",
|
|
3651
|
-
"budget",
|
|
3652
|
-
"output-limit",
|
|
3653
|
-
"provider",
|
|
3654
|
-
"persistence",
|
|
3655
|
-
"validation",
|
|
3656
|
-
"verification",
|
|
3657
|
-
"internal"
|
|
3658
|
-
]);
|
|
3659
3868
|
for (const entry of entries) {
|
|
3660
|
-
if (
|
|
3869
|
+
if (isTelemetryFailureKind(entry.failureKind)) {
|
|
3661
3870
|
failures[entry.failureKind] = (failures[entry.failureKind] ?? 0) + 1;
|
|
3662
3871
|
}
|
|
3663
3872
|
}
|
|
@@ -3807,7 +4016,7 @@ function canaryRows(insights) {
|
|
|
3807
4016
|
const baseline = insights.canary.baseline;
|
|
3808
4017
|
const canary = insights.canary.canary;
|
|
3809
4018
|
const rows = [
|
|
3810
|
-
["Runs", metricNum(baseline.runs), metricNum(canary.runs)],
|
|
4019
|
+
["Runs (total/applied)", metricNum(baseline.runs) + "/" + metricNum(baseline.appliedRuns), metricNum(canary.runs) + "/" + metricNum(canary.appliedRuns)],
|
|
3811
4020
|
["Success", metricPct(baseline.successRate), metricPct(canary.successRate)],
|
|
3812
4021
|
["Verify quality", baseline.avgQuality?.toFixed(1) ?? "\u2014", canary.avgQuality?.toFixed(1) ?? "\u2014"],
|
|
3813
4022
|
["p95 duration", metricMs(baseline.p95LatencyMs), metricMs(canary.p95LatencyMs)],
|
|
@@ -3913,7 +4122,7 @@ function buildMetricsReport(entries = readMetricsLog(100), damageEntries, prebui
|
|
|
3913
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,
|
|
3914
4123
|
"",
|
|
3915
4124
|
"## Canary / stable control",
|
|
3916
|
-
"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,
|
|
3917
4126
|
...canary.reasons.map((item) => "- " + item),
|
|
3918
4127
|
...canary.triggers.map((item) => "- Trigger " + item.metric + ": stable " + item.baseline + " \u2192 canary " + item.canary + " (" + item.threshold + ")"),
|
|
3919
4128
|
"",
|
|
@@ -3955,7 +4164,7 @@ function writeMetricsDashboard(entries = readMetricsLog(200), damageEntries = re
|
|
|
3955
4164
|
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
3956
4165
|
${metricCard("Data Confidence", insights.confidence.score + "/100", `telemetry completeness \xB7 target \u226585 ${insights.confidence.targetMet ? "met" : "not met"}`, confidenceTone)}
|
|
3957
4166
|
${metricCard("Quality Health", insights.quality.healthScore + "/100", `actual outcomes \xB7 target \u226585 ${insights.quality.targetMet ? "met" : "not met"}`, qualityTone)}
|
|
3958
|
-
${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)}
|
|
3959
4168
|
</section>
|
|
3960
4169
|
<section class="layout">
|
|
3961
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>
|
|
@@ -4151,6 +4360,9 @@ function releaseRunLock(lock, sessionId) {
|
|
|
4151
4360
|
// src/infra/session-identity.ts
|
|
4152
4361
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4153
4362
|
var UNRESOLVED_PREFIX = "unresolved:";
|
|
4363
|
+
function branchEntryIds(branch) {
|
|
4364
|
+
return Array.from(branch, (entry) => entry.id).filter((id) => typeof id === "string");
|
|
4365
|
+
}
|
|
4154
4366
|
function resolveSessionId(ctx) {
|
|
4155
4367
|
const resolved = ctx.sessionManager?.getSessionId?.();
|
|
4156
4368
|
if (typeof resolved === "string" && resolved.length > 0)
|
|
@@ -4179,7 +4391,12 @@ function findGitRoot(cwd) {
|
|
|
4179
4391
|
return ROOT_CACHE.get(cwd) ?? null;
|
|
4180
4392
|
let root = null;
|
|
4181
4393
|
try {
|
|
4182
|
-
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
|
+
});
|
|
4183
4400
|
root = out.trim() || null;
|
|
4184
4401
|
} catch (e) {
|
|
4185
4402
|
debug("git rev-parse failed for " + cwd, e);
|
|
@@ -4278,7 +4495,13 @@ function deriveFromRelativePaths(paths) {
|
|
|
4278
4495
|
return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
|
|
4279
4496
|
}
|
|
4280
4497
|
function deriveProjectIdFromCwd(cwd) {
|
|
4281
|
-
|
|
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);
|
|
4282
4505
|
}
|
|
4283
4506
|
function deriveProjectId(cwd, extraction, sessionId) {
|
|
4284
4507
|
if (cwd && cwd !== "/" && cwd !== process.env.HOME) {
|
|
@@ -4351,24 +4574,35 @@ function loadProjectFingerprint(projectId) {
|
|
|
4351
4574
|
}
|
|
4352
4575
|
function saveProjectFingerprint(projectId, extraction) {
|
|
4353
4576
|
try {
|
|
4354
|
-
const
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
keyDirectories
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
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
|
+
}
|
|
4372
4606
|
} catch (e) {
|
|
4373
4607
|
warn("saveProjectFingerprint failed", e);
|
|
4374
4608
|
}
|
|
@@ -4576,6 +4810,22 @@ function advance(rc, _stage) {
|
|
|
4576
4810
|
}
|
|
4577
4811
|
|
|
4578
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
|
+
}
|
|
4579
4829
|
function planCompactionWindow(input) {
|
|
4580
4830
|
const {
|
|
4581
4831
|
msgs,
|
|
@@ -4593,7 +4843,8 @@ function planCompactionWindow(input) {
|
|
|
4593
4843
|
const fixedContextTokens = overflowedContext ? 0 : Math.max(0, totalTokens - allMessageTokens);
|
|
4594
4844
|
const adaptiveKeepTokens = modelContextWindow ? Math.min(profileCfg.keepRecentTokens * 2, Math.max(profileCfg.keepRecentTokens, modelContextWindow * 0.04)) : profileCfg.keepRecentTokens;
|
|
4595
4845
|
const targetPercent = MODE_POLICIES[mode].targetContextPercent;
|
|
4596
|
-
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;
|
|
4597
4848
|
const retentionCeiling = force ? adaptiveKeepTokens : Math.max(adaptiveKeepTokens, targetRetainedTokens);
|
|
4598
4849
|
const rawMinimumTail = adaptiveKeepTokens / messageScale;
|
|
4599
4850
|
const rawRetentionCeiling = retentionCeiling / messageScale;
|
|
@@ -4636,10 +4887,10 @@ function planCompactionWindow(input) {
|
|
|
4636
4887
|
hardBoundaryAdjusted ||= keepFrom !== boundaryBeforeHardGuard;
|
|
4637
4888
|
const compactTokens = Math.round(messageTokens.slice(0, keepFrom).reduce((sum, tokens) => sum + tokens, 0) * messageScale);
|
|
4638
4889
|
const retainedTokens = retainedAt(keepFrom);
|
|
4639
|
-
const projectedAfterTokens = fixedContextTokens + retainedTokens + profileCfg.summaryBudgetTokens;
|
|
4890
|
+
const projectedAfterTokens = fixedContextTokens + retainedTokens + profileCfg.summaryBudgetTokens + postSummaryReserveTokens;
|
|
4640
4891
|
const projectedSavedTokens = Math.max(0, totalTokens - projectedAfterTokens);
|
|
4641
4892
|
const projectedYield = totalTokens > 0 ? projectedSavedTokens / totalTokens : 0;
|
|
4642
|
-
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + profileCfg.summaryBudgetTokens;
|
|
4893
|
+
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + profileCfg.summaryBudgetTokens + postSummaryReserveTokens;
|
|
4643
4894
|
let reason = "viable";
|
|
4644
4895
|
if (msgs[keepFrom]?.message?.role === "toolResult")
|
|
4645
4896
|
reason = "unsafe-tool-boundary";
|
|
@@ -4693,8 +4944,7 @@ function resolveCompactionWindow(rc) {
|
|
|
4693
4944
|
});
|
|
4694
4945
|
if (!plan.viable) {
|
|
4695
4946
|
if (rc.flags.force) {
|
|
4696
|
-
|
|
4697
|
-
rc.notify("Manual compaction skipped: " + detail, "warning");
|
|
4947
|
+
rc.notify("Manual compaction skipped: " + compactionPlanReasonText(plan.reason) + ".", "warning");
|
|
4698
4948
|
} else {
|
|
4699
4949
|
rc.notify("Smart compact skipped: the safe plan cannot meet its target; using native compaction instead.", "warning");
|
|
4700
4950
|
}
|
|
@@ -4703,281 +4953,130 @@ function resolveCompactionWindow(rc) {
|
|
|
4703
4953
|
if (overflowedContext && plan.relaxedSoftBoundaries.length) {
|
|
4704
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");
|
|
4705
4955
|
}
|
|
4706
|
-
const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
|
|
4707
|
-
if (rc.flags.force && rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
|
|
4708
|
-
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");
|
|
4709
|
-
}
|
|
4710
|
-
const out = rc;
|
|
4711
|
-
out.sessionId = resolveSessionId(rc.ctx);
|
|
4712
|
-
out.branch = branch;
|
|
4713
|
-
out.msgs = msgs;
|
|
4714
|
-
out.totalTokens = totalTokens;
|
|
4715
|
-
out.contextPercent = contextPercent;
|
|
4716
|
-
out.toolPercent = 0;
|
|
4717
|
-
out.keepFrom = plan.keepFrom;
|
|
4718
|
-
out.toCompact = msgs.slice(0, plan.keepFrom);
|
|
4719
|
-
out.firstKeptId = msgs[plan.keepFrom].id;
|
|
4720
|
-
out.compactTokens = plan.compactTokens;
|
|
4721
|
-
out.accTokens = plan.retainedTokens;
|
|
4722
|
-
out.compactionPlan = plan;
|
|
4723
|
-
return advance(out, "_windowed");
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
// src/app/preflight.ts
|
|
4727
|
-
function preflightDamageMedian(cwd, config) {
|
|
4728
|
-
if (!config.adaptiveDamageFeedback)
|
|
4729
|
-
return 0;
|
|
4730
|
-
const recent = readRecentDamageScores(deriveProjectIdFromCwd(cwd), 5).slice(-3).sort((a, b) => a - b);
|
|
4731
|
-
return recent.length ? recent[Math.floor(recent.length / 2)] : 0;
|
|
4732
|
-
}
|
|
4733
|
-
function preparePreflightProfile(input) {
|
|
4734
|
-
const config = input.config;
|
|
4735
|
-
const profile = MODE_POLICIES[input.mode].profile;
|
|
4736
|
-
let profileCfg = { ...PROFILES[profile], ...config.profiles?.[profile] ?? {} };
|
|
4737
|
-
const damageMedian = input.damageMedian ?? preflightDamageMedian(input.cwd, config);
|
|
4738
|
-
if (damageMedian >= 25) {
|
|
4739
|
-
profileCfg = {
|
|
4740
|
-
...profileCfg,
|
|
4741
|
-
keepRecentTokens: Math.round(profileCfg.keepRecentTokens * (damageMedian >= 50 ? 1.5 : 1.25)),
|
|
4742
|
-
summaryBudgetTokens: Math.round(profileCfg.summaryBudgetTokens * (damageMedian >= 50 ? 1.3 : 1.2))
|
|
4743
|
-
};
|
|
4744
|
-
}
|
|
4745
|
-
return {
|
|
4746
|
-
profileCfg,
|
|
4747
|
-
estimator: makeTokenEstimator(input.summaryModel.provider, input.summaryModel.id, input.tokenCalibration),
|
|
4748
|
-
adapted: damageMedian >= 25,
|
|
4749
|
-
damageMedian
|
|
4750
|
-
};
|
|
4751
|
-
}
|
|
4752
|
-
function planManualPreflight(ctx, summaryModel, mode, tokenCalibration, config, damageMedian) {
|
|
4753
|
-
const prepared = preparePreflightProfile({ cwd: ctx.cwd, summaryModel, mode, tokenCalibration, config, damageMedian });
|
|
4754
|
-
const manager = ctx.sessionManager;
|
|
4755
|
-
const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
|
|
4756
|
-
const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
|
|
4757
|
-
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
4758
|
-
const contextWindowTokens = ctx.model?.contextWindow ?? 0;
|
|
4759
|
-
const contextPercent = contextWindowTokens > 0 ? totalTokens / contextWindowTokens * 100 : 0;
|
|
4760
|
-
const toolPercent = computeToolCharPercentage(branch);
|
|
4761
|
-
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
4762
|
-
const messageTokens = msgs.map((entry) => prepared.estimator.message(entry.message));
|
|
4763
|
-
const rawEstimatedMessageTokens = messageTokens.reduce((sum, tokens) => sum + tokens, 0);
|
|
4764
|
-
if (msgs.length < 3) {
|
|
4765
|
-
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 };
|
|
4766
|
-
}
|
|
4767
|
-
const plan = planCompactionWindow({
|
|
4768
|
-
msgs,
|
|
4769
|
-
branch,
|
|
4770
|
-
messageTokens,
|
|
4771
|
-
totalTokens,
|
|
4772
|
-
modelContextWindow: ctx.model?.contextWindow,
|
|
4773
|
-
mode,
|
|
4774
|
-
profileCfg: prepared.profileCfg,
|
|
4775
|
-
force: true,
|
|
4776
|
-
overflowedContext
|
|
4777
|
-
});
|
|
4778
|
-
const normalizedMessages = plan.compactTokens + plan.retainedTokens;
|
|
4779
|
-
return {
|
|
4780
|
-
mode,
|
|
4781
|
-
plan,
|
|
4782
|
-
reason: plan.reason,
|
|
4783
|
-
profileCfg: prepared.profileCfg,
|
|
4784
|
-
totalTokens,
|
|
4785
|
-
rawEstimatedMessageTokens,
|
|
4786
|
-
estimatorScale: rawEstimatedMessageTokens > 0 ? normalizedMessages / rawEstimatedMessageTokens : 1,
|
|
4787
|
-
adapted: prepared.adapted,
|
|
4788
|
-
damageMedian: prepared.damageMedian,
|
|
4789
|
-
contextWindowTokens,
|
|
4790
|
-
contextPercent,
|
|
4791
|
-
toolPercent,
|
|
4792
|
-
overflowedContext
|
|
4793
|
-
};
|
|
4794
|
-
}
|
|
4795
|
-
|
|
4796
|
-
// src/ui/overlays.ts
|
|
4797
|
-
import path8 from "path";
|
|
4798
|
-
|
|
4799
|
-
// src/utils/state.ts
|
|
4800
|
-
import fs5 from "fs";
|
|
4801
|
-
|
|
4802
|
-
// src/domain/summary-schema.ts
|
|
4803
|
-
function classifyHeading(raw) {
|
|
4804
|
-
const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
|
|
4805
|
-
if (!text)
|
|
4806
|
-
return "unknown";
|
|
4807
|
-
if (text === "goal" || text === "goals" || text === "objective" || text === "objectives")
|
|
4808
|
-
return "goal";
|
|
4809
|
-
if (text.startsWith("constraint") || text.includes("preference"))
|
|
4810
|
-
return "constraints";
|
|
4811
|
-
if (text === "progress" || text === "status")
|
|
4812
|
-
return "progress";
|
|
4813
|
-
if (text.includes("key decision") || text === "decisions")
|
|
4814
|
-
return "decisions";
|
|
4815
|
-
if (text.includes("file") && text.includes("modif"))
|
|
4816
|
-
return "files-modified";
|
|
4817
|
-
if (text.includes("file") && (text.includes("read") || text.includes("viewed")))
|
|
4818
|
-
return "files-read";
|
|
4819
|
-
if (text.includes("next step") || text === "next actions")
|
|
4820
|
-
return "next-steps";
|
|
4821
|
-
if (text.includes("critical context") || text === "important context")
|
|
4822
|
-
return "critical-context";
|
|
4823
|
-
if (text === "topics" || text.includes("topics covered"))
|
|
4824
|
-
return "topics";
|
|
4825
|
-
if (text.includes("open loop") || text.includes("unresolved"))
|
|
4826
|
-
return "open-loops";
|
|
4827
|
-
if (text.includes("changes since") || text === "changes")
|
|
4828
|
-
return "changes";
|
|
4829
|
-
if (text.includes("verification"))
|
|
4830
|
-
return "verification-note";
|
|
4831
|
-
return "unknown";
|
|
4832
|
-
}
|
|
4833
|
-
function canonicalHeading(kind) {
|
|
4834
|
-
switch (kind) {
|
|
4835
|
-
case "goal":
|
|
4836
|
-
return SECTION_GOAL;
|
|
4837
|
-
case "constraints":
|
|
4838
|
-
return SECTION_CONSTRAINTS;
|
|
4839
|
-
case "progress":
|
|
4840
|
-
return SECTION_PROGRESS;
|
|
4841
|
-
case "decisions":
|
|
4842
|
-
return SECTION_DECISIONS;
|
|
4843
|
-
case "files-modified":
|
|
4844
|
-
return SECTION_FILES_MODIFIED;
|
|
4845
|
-
case "files-read":
|
|
4846
|
-
return SECTION_FILES_READ;
|
|
4847
|
-
case "next-steps":
|
|
4848
|
-
return SECTION_NEXT_STEPS;
|
|
4849
|
-
case "critical-context":
|
|
4850
|
-
return SECTION_CRITICAL_CONTEXT;
|
|
4851
|
-
case "topics":
|
|
4852
|
-
return SECTION_TOPICS;
|
|
4853
|
-
case "open-loops":
|
|
4854
|
-
return SECTION_OPEN_LOOPS;
|
|
4855
|
-
case "changes":
|
|
4856
|
-
return SECTION_CHANGES;
|
|
4857
|
-
case "verification-note":
|
|
4858
|
-
return "## Verification Note";
|
|
4859
|
-
case "unknown":
|
|
4860
|
-
default:
|
|
4861
|
-
return "## Section";
|
|
4862
|
-
}
|
|
4863
|
-
}
|
|
4864
|
-
|
|
4865
|
-
// src/domain/summary-parse.ts
|
|
4866
|
-
var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
4867
|
-
function summaryEvidenceLine(value, maxLength) {
|
|
4868
|
-
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
|
|
4869
|
-
}
|
|
4870
|
-
function mergeBodies(first, second) {
|
|
4871
|
-
const seen = new Set;
|
|
4872
|
-
return [first, second].filter(Boolean).flatMap((body) => body.split(`
|
|
4873
|
-
`)).filter((line) => seen.has(line) ? false : (seen.add(line), true)).join(`
|
|
4874
|
-
`).trim();
|
|
4875
|
-
}
|
|
4876
|
-
function parseSummary(markdown) {
|
|
4877
|
-
const sections = [];
|
|
4878
|
-
const lines = markdown.split(`
|
|
4879
|
-
`);
|
|
4880
|
-
let currentHeading = "";
|
|
4881
|
-
let currentKind = "unknown";
|
|
4882
|
-
let bodyLines = [];
|
|
4883
|
-
let started = false;
|
|
4884
|
-
const flush = () => {
|
|
4885
|
-
if (!started)
|
|
4886
|
-
return;
|
|
4887
|
-
const body = bodyLines.join(`
|
|
4888
|
-
`).trim();
|
|
4889
|
-
const existing = currentKind === "unknown" ? undefined : sections.find((s) => s.kind === currentKind);
|
|
4890
|
-
if (existing)
|
|
4891
|
-
existing.body = mergeBodies(existing.body, body);
|
|
4892
|
-
else
|
|
4893
|
-
sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
|
|
4894
|
-
};
|
|
4895
|
-
for (const line of lines) {
|
|
4896
|
-
const m = line.match(HEADING_RE);
|
|
4897
|
-
if (m) {
|
|
4898
|
-
const kind = classifyHeading(m[2]);
|
|
4899
|
-
if (m[1].length <= 2 || kind !== "unknown") {
|
|
4900
|
-
flush();
|
|
4901
|
-
currentHeading = "## " + m[2].trim();
|
|
4902
|
-
currentKind = kind;
|
|
4903
|
-
bodyLines = [];
|
|
4904
|
-
started = true;
|
|
4905
|
-
continue;
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
if (started)
|
|
4909
|
-
bodyLines.push(line);
|
|
4910
|
-
}
|
|
4911
|
-
flush();
|
|
4912
|
-
return { sections };
|
|
4913
|
-
}
|
|
4914
|
-
function findSection(summary, kind) {
|
|
4915
|
-
const parsed = typeof summary === "string" ? parseSummary(summary) : summary;
|
|
4916
|
-
return parsed.sections.find((s) => s.kind === kind);
|
|
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");
|
|
4917
4974
|
}
|
|
4918
|
-
function renderSummary(summary, opts = {}) {
|
|
4919
|
-
return summary.sections.map((s) => {
|
|
4920
|
-
const heading = opts.canonicalHeadings && s.kind !== "unknown" ? canonicalHeading(s.kind) : s.heading;
|
|
4921
|
-
return heading + `
|
|
4922
|
-
` + s.body;
|
|
4923
|
-
}).join(`
|
|
4924
4975
|
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
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;
|
|
4929
4985
|
}
|
|
4930
|
-
function
|
|
4931
|
-
const
|
|
4932
|
-
const
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
const idx = summary.sections.findIndex((s) => s.kind === hint.before);
|
|
4942
|
-
if (idx >= 0) {
|
|
4943
|
-
const sections = summary.sections.slice();
|
|
4944
|
-
sections.splice(idx, 0, section);
|
|
4945
|
-
return { sections };
|
|
4946
|
-
}
|
|
4947
|
-
}
|
|
4948
|
-
if (hint.after) {
|
|
4949
|
-
let idx = -1;
|
|
4950
|
-
for (let i = summary.sections.length - 1;i >= 0; i--) {
|
|
4951
|
-
if (summary.sections[i].kind === hint.after) {
|
|
4952
|
-
idx = i;
|
|
4953
|
-
break;
|
|
4954
|
-
}
|
|
4955
|
-
}
|
|
4956
|
-
if (idx >= 0) {
|
|
4957
|
-
const sections = summary.sections.slice();
|
|
4958
|
-
sections.splice(idx + 1, 0, section);
|
|
4959
|
-
return { sections };
|
|
4960
|
-
}
|
|
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
|
+
};
|
|
4961
4997
|
}
|
|
4962
|
-
return {
|
|
4998
|
+
return {
|
|
4999
|
+
profileCfg,
|
|
5000
|
+
estimator: makeTokenEstimator(input.summaryModel.provider, input.summaryModel.id, input.tokenCalibration),
|
|
5001
|
+
adapted: damageMedian >= 25,
|
|
5002
|
+
damageMedian
|
|
5003
|
+
};
|
|
4963
5004
|
}
|
|
4964
|
-
function
|
|
4965
|
-
const
|
|
4966
|
-
const
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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 };
|
|
4975
5045
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
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
|
+
};
|
|
4978
5073
|
}
|
|
4979
5074
|
|
|
5075
|
+
// src/ui/overlays.ts
|
|
5076
|
+
import path8 from "path";
|
|
5077
|
+
|
|
4980
5078
|
// src/utils/state.ts
|
|
5079
|
+
import fs5 from "fs";
|
|
4981
5080
|
function getStatePath(projectId, state) {
|
|
4982
5081
|
return state?.scope?.sessionId ? scopedCompactionStateFile(projectId, state.scope.sessionId) : compactionStateFile(projectId);
|
|
4983
5082
|
}
|
|
@@ -4986,12 +5085,16 @@ function isLegacySearchOutput(text) {
|
|
|
4986
5085
|
return /^[^\s:][^:]*:\d+(?::\d+)?:/.test(firstLine);
|
|
4987
5086
|
}
|
|
4988
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;
|
|
4989
5090
|
const constraints = state.constraints.filter((item) => !isDiagnosticConstraintText(item.text));
|
|
4990
|
-
const unresolvedErrors = state.unresolvedErrors.filter((item) => !
|
|
4991
|
-
const
|
|
4992
|
-
|
|
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)
|
|
4993
5096
|
return state;
|
|
4994
|
-
return { ...state, constraints, unresolvedErrors, openLoops };
|
|
5097
|
+
return { ...state, goal, constraints, unresolvedErrors, resolvedErrors, openLoops, criticalContext };
|
|
4995
5098
|
}
|
|
4996
5099
|
function freshState(fp, data) {
|
|
4997
5100
|
if (!data)
|
|
@@ -5014,14 +5117,14 @@ function saveCompactionState(projectId, state) {
|
|
|
5014
5117
|
warn("saveCompactionState failed", e);
|
|
5015
5118
|
}
|
|
5016
5119
|
}
|
|
5017
|
-
function loadScopedCompactionState(scope,
|
|
5120
|
+
function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
5018
5121
|
const fp = scopedCompactionStateFile(scope.projectId, scope.sessionId);
|
|
5019
5122
|
const state = freshState(fp, readJsonSync(fp));
|
|
5020
5123
|
if (!state?.scope || state.scope.schemaVersion !== 2)
|
|
5021
5124
|
return null;
|
|
5022
5125
|
if (state.scope.projectId !== scope.projectId || state.scope.sessionId !== scope.sessionId)
|
|
5023
5126
|
return null;
|
|
5024
|
-
if (state.scope.branchHeadId &&
|
|
5127
|
+
if (state.scope.branchHeadId && branchEntryIds2.length > 0 && !branchEntryIds2.includes(state.scope.branchHeadId))
|
|
5025
5128
|
return null;
|
|
5026
5129
|
return state;
|
|
5027
5130
|
}
|
|
@@ -5125,26 +5228,34 @@ function mergeBy(current, previous, key, limit) {
|
|
|
5125
5228
|
return true;
|
|
5126
5229
|
}).slice(0, limit);
|
|
5127
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
|
+
}
|
|
5128
5235
|
function mergeCompactionStates(previous, current) {
|
|
5129
|
-
if (!previous)
|
|
5130
|
-
|
|
5236
|
+
if (!previous) {
|
|
5237
|
+
const active = applyContinuityOverrides(current, current.factOverrides ?? []);
|
|
5238
|
+
return { ...active, openLoops: mergeOpenLoops(active.openLoops, []) };
|
|
5239
|
+
}
|
|
5131
5240
|
const factOverrides = mergeBy(current.factOverrides ?? [], previous.factOverrides ?? [], (item) => item.kind + ":" + item.summaryKey, 50);
|
|
5132
5241
|
const activeCurrent = applyContinuityOverrides(current, factOverrides);
|
|
5133
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));
|
|
5134
5245
|
const resolvedKeys = new Set(activeCurrent.resolvedErrors.map((error2) => normalizeFactKey(error2.message)));
|
|
5135
5246
|
const decisions = mergeBy(activeCurrent.decisions, activePrevious.decisions, (item) => normalizeFactKey(item.summary), 30).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) }));
|
|
5136
5247
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
5137
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) }));
|
|
5138
|
-
const openLoops =
|
|
5249
|
+
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops);
|
|
5139
5250
|
const oldGoal = activePrevious.goal && activeCurrent.goal && normalizeFactKey(activePrevious.goal) !== normalizeFactKey(activeCurrent.goal) ? ["Previous goal: " + activePrevious.goal] : [];
|
|
5140
5251
|
return applyContinuityOverrides({
|
|
5141
5252
|
...activeCurrent,
|
|
5142
5253
|
goal: activeCurrent.goal ?? activePrevious.goal,
|
|
5143
5254
|
decisions,
|
|
5144
5255
|
constraints,
|
|
5145
|
-
modifiedFiles: mergeBy(activeCurrent.modifiedFiles, activePrevious.modifiedFiles, normalizeFactKey, 100),
|
|
5146
|
-
readFiles: mergeBy(activeCurrent.readFiles, activePrevious.readFiles, normalizeFactKey, 100),
|
|
5147
|
-
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),
|
|
5148
5259
|
unresolvedErrors,
|
|
5149
5260
|
resolvedErrors: mergeBy(activeCurrent.resolvedErrors, activePrevious.resolvedErrors, (item) => normalizeFactKey(item.message), 20),
|
|
5150
5261
|
openLoops,
|
|
@@ -5198,28 +5309,37 @@ function injectOpenLoopsSection(summary, openLoops) {
|
|
|
5198
5309
|
return renderSummary(updated);
|
|
5199
5310
|
}
|
|
5200
5311
|
function computeDelta(prev, current) {
|
|
5201
|
-
const
|
|
5202
|
-
const
|
|
5203
|
-
const
|
|
5204
|
-
const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(
|
|
5205
|
-
const
|
|
5206
|
-
const
|
|
5207
|
-
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
|
+
]);
|
|
5208
5325
|
const resolvedLoops = [];
|
|
5209
5326
|
const persistentLoops = [];
|
|
5210
5327
|
for (const [k, loop] of prevLoopSummaries) {
|
|
5211
5328
|
if (currLoopKeys.has(k))
|
|
5212
5329
|
persistentLoops.push(loop.summary);
|
|
5213
|
-
else
|
|
5330
|
+
else if (resolvedLoopKeys.has(k))
|
|
5214
5331
|
resolvedLoops.push(loop.summary);
|
|
5215
5332
|
}
|
|
5216
|
-
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);
|
|
5217
5334
|
const prevFiles = new Set(prev.modifiedFiles);
|
|
5218
5335
|
const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
|
|
5219
|
-
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) =>
|
|
5220
|
-
const
|
|
5221
|
-
|
|
5222
|
-
|
|
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);
|
|
5223
5343
|
const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
|
|
5224
5344
|
return {
|
|
5225
5345
|
newDecisions,
|
|
@@ -5393,22 +5513,7 @@ var MODE_COPY = {
|
|
|
5393
5513
|
thorough: "deepest analysis \xB7 rich 30K recent tail \xB7 10K summary"
|
|
5394
5514
|
};
|
|
5395
5515
|
function explainPreflightReason(reason) {
|
|
5396
|
-
|
|
5397
|
-
case "viable":
|
|
5398
|
-
return "safe window and useful estimated saving";
|
|
5399
|
-
case "not-enough-messages":
|
|
5400
|
-
return "fewer than 3 active messages";
|
|
5401
|
-
case "no-eligible-prefix":
|
|
5402
|
-
return "no older prefix is available";
|
|
5403
|
-
case "unsafe-tool-boundary":
|
|
5404
|
-
return "no complete tool-call boundary is available";
|
|
5405
|
-
case "retention-target-exceeded":
|
|
5406
|
-
return "a complete tool pair exceeds the tail target";
|
|
5407
|
-
case "mode-target-not-met":
|
|
5408
|
-
return "the estimated result misses this preset's target";
|
|
5409
|
-
case "insufficient-projected-saving":
|
|
5410
|
-
return "estimated saving is below 10%";
|
|
5411
|
-
}
|
|
5516
|
+
return reason === "not-enough-messages" ? "fewer than 3 active messages" : compactionPlanReasonText(reason);
|
|
5412
5517
|
}
|
|
5413
5518
|
function recommendationEvidence(preflight) {
|
|
5414
5519
|
const yieldPercent = Math.round((preflight.plan?.projectedYield ?? 0) * 100);
|
|
@@ -5468,9 +5573,10 @@ function formatPreflightSummary(preflight, modelLabel, details = false) {
|
|
|
5468
5573
|
lines2.push("Estimator messages ~" + tokenCount(preflight.rawEstimatedMessageTokens) + " \xB7 normalization unavailable", "Route " + modelLabel + " \xB7 viability " + preflight.reason);
|
|
5469
5574
|
return lines2;
|
|
5470
5575
|
}
|
|
5576
|
+
const stateReserve = Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO);
|
|
5471
5577
|
const lines = [
|
|
5472
5578
|
"Plan " + compactTokenCount(preflight.totalTokens) + " \u2192 ~" + compactTokenCount(plan.projectedAfterTokens) + " \xB7 ~" + compactTokenCount(plan.projectedSavedTokens) + " saved (" + percent(plan.projectedYield * 100) + ")",
|
|
5473
|
-
"Keep ~" + compactTokenCount(plan.retainedTokens) + " recent \xB7 summary up to " + compactTokenCount(plan.summaryBudgetTokens),
|
|
5579
|
+
"Keep ~" + compactTokenCount(plan.retainedTokens) + " recent \xB7 summary up to " + compactTokenCount(plan.summaryBudgetTokens) + " + ~" + compactTokenCount(stateReserve) + " verified-state reserve",
|
|
5474
5580
|
"\u2713 Complete tool pairs \xB7 \u2713 zero-gap verification before apply"
|
|
5475
5581
|
];
|
|
5476
5582
|
if (!plan.viable)
|
|
@@ -5519,8 +5625,11 @@ function notifyAppliedCompaction(ctx, details, concise) {
|
|
|
5519
5625
|
const after = details.estimatedAfterTokens ?? Math.max(0, before - details.tokensSaved);
|
|
5520
5626
|
const saving = Math.round((details.estimatedYield ?? (before ? details.tokensSaved / before : 0)) * 100);
|
|
5521
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";
|
|
5522
5631
|
const planned = details.plannedAfterTokens ?? after;
|
|
5523
|
-
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");
|
|
5524
5633
|
}
|
|
5525
5634
|
async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
5526
5635
|
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
@@ -5542,10 +5651,13 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
5542
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));
|
|
5543
5652
|
}
|
|
5544
5653
|
const scoreColor = details.qualityScore >= 80 ? "success" : details.qualityScore >= 50 ? "warning" : "error";
|
|
5545
|
-
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));
|
|
5546
5655
|
if (details.provenance) {
|
|
5547
5656
|
const provenance = details.provenance;
|
|
5548
|
-
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
|
+
}
|
|
5549
5661
|
}
|
|
5550
5662
|
if ((details.redactions ?? 0) > 0) {
|
|
5551
5663
|
c.addChild(new Text(theme.fg("warning", " Security: " + details.redactions + " sensitive value(s) redacted"), 0, 0));
|
|
@@ -5641,7 +5753,7 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
5641
5753
|
}, { overlay: true, overlayOptions: { width: "70%", anchor: "center", maxHeight: "80%" } });
|
|
5642
5754
|
if (!opts.approval)
|
|
5643
5755
|
return "closed";
|
|
5644
|
-
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.
|
|
5645
5757
|
|
|
5646
5758
|
Cancel keeps the current conversation unchanged.`);
|
|
5647
5759
|
return approved ? "apply" : "cancel";
|
|
@@ -5781,7 +5893,11 @@ async function showCompactUI(ctx, opts) {
|
|
|
5781
5893
|
const calibration = createProductionServices().tokenCalibration;
|
|
5782
5894
|
const damageMedian = preflightDamageMedian(ctx.cwd, opts.config);
|
|
5783
5895
|
while (true) {
|
|
5784
|
-
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
|
+
]));
|
|
5785
5901
|
const recommended = recommendPreflight(plans);
|
|
5786
5902
|
const action = await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
5787
5903
|
let selected = Math.max(0, PRIMARY_MODES.indexOf(recommended.mode));
|
|
@@ -6088,29 +6204,6 @@ function asSerializableMessages(msgs) {
|
|
|
6088
6204
|
import * as fs6 from "fs";
|
|
6089
6205
|
import * as path9 from "path";
|
|
6090
6206
|
import { StringDecoder } from "string_decoder";
|
|
6091
|
-
|
|
6092
|
-
// src/utils/lru.ts
|
|
6093
|
-
function lruGet(m, key) {
|
|
6094
|
-
if (!m.has(key))
|
|
6095
|
-
return;
|
|
6096
|
-
const v = m.get(key);
|
|
6097
|
-
m.delete(key);
|
|
6098
|
-
m.set(key, v);
|
|
6099
|
-
return v;
|
|
6100
|
-
}
|
|
6101
|
-
function lruSet(m, key, value, max) {
|
|
6102
|
-
if (m.has(key))
|
|
6103
|
-
m.delete(key);
|
|
6104
|
-
m.set(key, value);
|
|
6105
|
-
while (m.size > max) {
|
|
6106
|
-
const oldest = m.keys().next().value;
|
|
6107
|
-
if (oldest === undefined)
|
|
6108
|
-
break;
|
|
6109
|
-
m.delete(oldest);
|
|
6110
|
-
}
|
|
6111
|
-
}
|
|
6112
|
-
|
|
6113
|
-
// src/utils/session-log.ts
|
|
6114
6207
|
function getSessionsDir() {
|
|
6115
6208
|
return sessionsDir();
|
|
6116
6209
|
}
|
|
@@ -6442,7 +6535,7 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
6442
6535
|
if (block.name === "multi_tool_use.parallel" && Array.isArray(block.arguments?.tool_uses)) {
|
|
6443
6536
|
const tools = block.arguments.tool_uses;
|
|
6444
6537
|
const retained = tools.filter((tool, toolIndex) => {
|
|
6445
|
-
const id =
|
|
6538
|
+
const id = nestedToolCallId(block.id, idx, toolIndex, tool.id);
|
|
6446
6539
|
return !removedToolCallIds.has(id);
|
|
6447
6540
|
});
|
|
6448
6541
|
if (retained.length !== tools.length)
|
|
@@ -6497,7 +6590,8 @@ import { serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
|
6497
6590
|
function extractWithCache(rc) {
|
|
6498
6591
|
const extractStepStart = Date.now();
|
|
6499
6592
|
const currentEntryIds = rc.toCompact.map((e) => e.id);
|
|
6500
|
-
const
|
|
6593
|
+
const selectedMessages = rc.llmMessages;
|
|
6594
|
+
const pruning = pruneRedundant(selectedMessages);
|
|
6501
6595
|
const currentKeptEntryIds = pruning.keptIndices.map((i) => currentEntryIds[i]).filter((id) => typeof id === "string");
|
|
6502
6596
|
if (pruning.prunedCount > 0) {
|
|
6503
6597
|
rc.notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
@@ -6508,7 +6602,12 @@ function extractWithCache(rc) {
|
|
|
6508
6602
|
const extractionStart = pruneEnd;
|
|
6509
6603
|
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
6510
6604
|
const convTokens = rc.estimator.text(convText);
|
|
6511
|
-
|
|
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
|
+
}
|
|
6512
6611
|
const prevContext = getPreviousCompactionContext(rc.branch);
|
|
6513
6612
|
const cachedExt = loadCachedExtraction(rc.sessionId);
|
|
6514
6613
|
let extraction;
|
|
@@ -6558,18 +6657,18 @@ function extractWithCache(rc) {
|
|
|
6558
6657
|
}
|
|
6559
6658
|
const projectCtx = buildProjectContext(fingerprint);
|
|
6560
6659
|
const manager = rc.ctx.sessionManager;
|
|
6561
|
-
const fullBranch = manager?.getBranch ?
|
|
6562
|
-
const
|
|
6660
|
+
const fullBranch = manager?.getBranch ? manager.getBranch() : rc.branch;
|
|
6661
|
+
const ancestryIds = branchEntryIds(fullBranch);
|
|
6563
6662
|
const continuityScope = {
|
|
6564
6663
|
schemaVersion: 2,
|
|
6565
6664
|
projectId,
|
|
6566
6665
|
sessionId: rc.sessionId,
|
|
6567
|
-
...
|
|
6568
|
-
branchHeadId:
|
|
6569
|
-
branchAncestryIds:
|
|
6666
|
+
...ancestryIds.length ? {
|
|
6667
|
+
branchHeadId: ancestryIds[ancestryIds.length - 1],
|
|
6668
|
+
branchAncestryIds: ancestryIds
|
|
6570
6669
|
} : {}
|
|
6571
6670
|
};
|
|
6572
|
-
const previousState = loadScopedCompactionState(continuityScope,
|
|
6671
|
+
const previousState = loadScopedCompactionState(continuityScope, ancestryIds);
|
|
6573
6672
|
const continuity = previousState ? renderContinuityCapsule(previousState) : "";
|
|
6574
6673
|
const out = rc;
|
|
6575
6674
|
out.pruning = pruning;
|
|
@@ -7006,6 +7105,7 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
7006
7105
|
return fallbackExplorationReport(llmMessages);
|
|
7007
7106
|
}
|
|
7008
7107
|
}
|
|
7108
|
+
|
|
7009
7109
|
// src/infra/synthesis-cache.ts
|
|
7010
7110
|
import { createHash as createHash2 } from "crypto";
|
|
7011
7111
|
var cache = new Map;
|
|
@@ -7034,7 +7134,7 @@ function synthesisCacheKey(rc) {
|
|
|
7034
7134
|
codexCallMs: rc.config.codexMaxCallMs,
|
|
7035
7135
|
latencyMs: rc.config.maxLatencyMs
|
|
7036
7136
|
},
|
|
7037
|
-
focus: rc.
|
|
7137
|
+
focus: rc.focus?.trim() || undefined,
|
|
7038
7138
|
zeroCall: rc.config.zeroCallEnabled !== false,
|
|
7039
7139
|
note: rc.userNote
|
|
7040
7140
|
});
|
|
@@ -7452,17 +7552,10 @@ async function summarizeConversation(rc) {
|
|
|
7452
7552
|
if (rc.requestedMode === "auto") {
|
|
7453
7553
|
const refined = resolveMode("auto", rc.contextPercent, extraction, continuityRisk(rc.previousState) + (rc.adapted ? 12 : 0));
|
|
7454
7554
|
if (refined !== rc.mode) {
|
|
7455
|
-
const oldBase = { ...PROFILES[rc.profile], ...rc.config.profiles?.[rc.profile] ?? {} };
|
|
7456
|
-
const keepScale = rc.profileCfg.keepRecentTokens / oldBase.keepRecentTokens;
|
|
7457
|
-
const summaryScale = rc.profileCfg.summaryBudgetTokens / oldBase.summaryBudgetTokens;
|
|
7458
7555
|
rc.mode = refined;
|
|
7459
|
-
rc.profile = MODE_POLICIES[refined].profile;
|
|
7460
|
-
rc.profileCfg = { ...PROFILES[rc.profile], ...rc.config.profiles?.[rc.profile] ?? {} };
|
|
7461
|
-
rc.profileCfg.keepRecentTokens = Math.round(rc.profileCfg.keepRecentTokens * keepScale);
|
|
7462
|
-
rc.profileCfg.summaryBudgetTokens = Math.round(rc.profileCfg.summaryBudgetTokens * summaryScale);
|
|
7463
7556
|
const policy2 = MODE_POLICIES[refined];
|
|
7464
7557
|
rc.services.budget.setLimits(rc.maxLlmCalls ?? effectiveBudget(rc.config.maxLlmCalls, policy2.maxLlmCalls), rc.maxLlmInputTokens ?? effectiveBudget(rc.config.maxLlmInputTokens, policy2.maxInputTokens), policy2.maxOutputTokens);
|
|
7465
|
-
rc.notify("Auto
|
|
7558
|
+
rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
|
|
7466
7559
|
}
|
|
7467
7560
|
}
|
|
7468
7561
|
const pc = rc.profileCfg;
|
|
@@ -7477,7 +7570,7 @@ async function summarizeConversation(rc) {
|
|
|
7477
7570
|
phaseName: "Synthesize",
|
|
7478
7571
|
detail: "Reusing the cached continuation summary \xB7 no LLM call"
|
|
7479
7572
|
});
|
|
7480
|
-
const hit = rc;
|
|
7573
|
+
const hit = advance(rc, "_synthesized");
|
|
7481
7574
|
hit.finalSummary = cached.finalSummary;
|
|
7482
7575
|
hit.method = cached.method;
|
|
7483
7576
|
hit.methodForMetrics = cached.method + "-cache";
|
|
@@ -7487,7 +7580,7 @@ async function summarizeConversation(rc) {
|
|
|
7487
7580
|
hit.explorationRounds = cached.explorationRounds;
|
|
7488
7581
|
hit.chunkCount = cached.chunkCount;
|
|
7489
7582
|
markMeasuredPhase(hit, "synthesize", synthPhaseStart);
|
|
7490
|
-
return
|
|
7583
|
+
return hit;
|
|
7491
7584
|
}
|
|
7492
7585
|
const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
|
|
7493
7586
|
conversationTokens: rc.convTokens,
|
|
@@ -7510,7 +7603,7 @@ async function summarizeConversation(rc) {
|
|
|
7510
7603
|
chunkCount: 0
|
|
7511
7604
|
});
|
|
7512
7605
|
rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
|
|
7513
|
-
const deterministic = rc;
|
|
7606
|
+
const deterministic = advance(rc, "_synthesized");
|
|
7514
7607
|
deterministic.finalSummary = finalSummary2;
|
|
7515
7608
|
deterministic.method = "heuristic";
|
|
7516
7609
|
deterministic.methodForMetrics = "zero-call";
|
|
@@ -7520,7 +7613,7 @@ async function summarizeConversation(rc) {
|
|
|
7520
7613
|
deterministic.explorationRounds = 0;
|
|
7521
7614
|
deterministic.chunkCount = 0;
|
|
7522
7615
|
markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
|
|
7523
|
-
return
|
|
7616
|
+
return deterministic;
|
|
7524
7617
|
}
|
|
7525
7618
|
const shouldSkipExplore = !policy.explore;
|
|
7526
7619
|
const convText = rc.convText;
|
|
@@ -7750,7 +7843,7 @@ async function summarizeConversation(rc) {
|
|
|
7750
7843
|
}
|
|
7751
7844
|
method = "eesv";
|
|
7752
7845
|
}
|
|
7753
|
-
const out = rc;
|
|
7846
|
+
const out = advance(rc, "_synthesized");
|
|
7754
7847
|
out.finalSummary = finalSummary;
|
|
7755
7848
|
out.method = method;
|
|
7756
7849
|
out.methodForMetrics = method;
|
|
@@ -7768,7 +7861,7 @@ async function summarizeConversation(rc) {
|
|
|
7768
7861
|
chunkCount
|
|
7769
7862
|
});
|
|
7770
7863
|
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
7771
|
-
return
|
|
7864
|
+
return out;
|
|
7772
7865
|
}
|
|
7773
7866
|
|
|
7774
7867
|
// src/phases/verify.ts
|
|
@@ -7842,6 +7935,17 @@ var CONDITION_MARKERS = new Set([
|
|
|
7842
7935
|
"gerekli",
|
|
7843
7936
|
"gerektirir"
|
|
7844
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
|
+
]);
|
|
7845
7949
|
var SEMANTIC_STOP = new Set([
|
|
7846
7950
|
"the",
|
|
7847
7951
|
"and",
|
|
@@ -7891,6 +7995,24 @@ function evidenceFragments(text) {
|
|
|
7891
7995
|
function hasNearbyMarker(tokens, anchor, markers) {
|
|
7892
7996
|
return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
|
|
7893
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
|
+
}
|
|
7894
8016
|
function semanticShape(source) {
|
|
7895
8017
|
const sourceTokens = semanticTokens(source);
|
|
7896
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))));
|
|
@@ -7909,11 +8031,14 @@ function hasSemanticEvidence(source, target) {
|
|
|
7909
8031
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7910
8032
|
if (overlap < required)
|
|
7911
8033
|
return false;
|
|
7912
|
-
|
|
8034
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
8035
|
+
if (negative && !targetNegative) {
|
|
7913
8036
|
const conditionalRestatement = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
7914
8037
|
if (!conditionalRestatement)
|
|
7915
8038
|
return false;
|
|
7916
8039
|
}
|
|
8040
|
+
if (!negative && targetNegative)
|
|
8041
|
+
return false;
|
|
7917
8042
|
if (conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token)))
|
|
7918
8043
|
return false;
|
|
7919
8044
|
return true;
|
|
@@ -7931,10 +8056,13 @@ function hasSemanticContradiction(source, target) {
|
|
|
7931
8056
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7932
8057
|
if (overlap < required)
|
|
7933
8058
|
return false;
|
|
7934
|
-
|
|
8059
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
8060
|
+
if (negative && !targetNegative) {
|
|
7935
8061
|
const validConditional = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
7936
8062
|
return !validConditional;
|
|
7937
8063
|
}
|
|
8064
|
+
if (!negative && targetNegative)
|
|
8065
|
+
return true;
|
|
7938
8066
|
return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
|
|
7939
8067
|
});
|
|
7940
8068
|
}
|
|
@@ -8344,6 +8472,10 @@ async function verifyAndPatch(rc) {
|
|
|
8344
8472
|
return advance(out, "_verified");
|
|
8345
8473
|
}
|
|
8346
8474
|
|
|
8475
|
+
// src/app/steps/state.ts
|
|
8476
|
+
import fs7 from "fs";
|
|
8477
|
+
import path10 from "path";
|
|
8478
|
+
|
|
8347
8479
|
// src/domain/yield-gate.ts
|
|
8348
8480
|
class YieldGateError extends Error {
|
|
8349
8481
|
reason;
|
|
@@ -8424,9 +8556,14 @@ function buildState(rc) {
|
|
|
8424
8556
|
const nextActions = extractNextActions(summary);
|
|
8425
8557
|
const criticalContextItems = extractCriticalContext(summary);
|
|
8426
8558
|
const currentState = buildCompactionState(extraction, managedLoops, rc.explorationReport, nextActions, criticalContextItems, loopOverrides);
|
|
8559
|
+
const summarizedGoal = summaryEvidenceLine(findSection(summary, "goal")?.body ?? "", TRUNC.MESSAGE);
|
|
8427
8560
|
currentState.scope = rc.continuityScope;
|
|
8428
8561
|
currentState.factOverrides = prevState?.factOverrides ?? [];
|
|
8429
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
|
+
});
|
|
8430
8567
|
if (preserve.length > 0) {
|
|
8431
8568
|
compactionState.readFiles = Array.from(new Set([...preserve, ...compactionState.readFiles])).slice(0, 100);
|
|
8432
8569
|
}
|
|
@@ -8439,6 +8576,8 @@ function buildState(rc) {
|
|
|
8439
8576
|
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
8440
8577
|
}
|
|
8441
8578
|
}
|
|
8579
|
+
if (summarizedGoal && currentState.goal)
|
|
8580
|
+
compactionState.goal = summarizedGoal;
|
|
8442
8581
|
const continuity = renderContinuityCapsule(compactionState, undefined, summary);
|
|
8443
8582
|
if (continuity)
|
|
8444
8583
|
summary = summary.trimEnd() + `
|
|
@@ -8512,8 +8651,8 @@ function buildState(rc) {
|
|
|
8512
8651
|
|
|
8513
8652
|
// src/infra/context-graph.ts
|
|
8514
8653
|
import { createHash as createHash3 } from "crypto";
|
|
8515
|
-
import
|
|
8516
|
-
import
|
|
8654
|
+
import fs8 from "fs";
|
|
8655
|
+
import path11 from "path";
|
|
8517
8656
|
import { createRequire } from "module";
|
|
8518
8657
|
var require2 = createRequire(import.meta.url);
|
|
8519
8658
|
var MAX_PROJECT_NODES = 2000;
|
|
@@ -8521,6 +8660,7 @@ var MAX_MANUAL_NODES = 500;
|
|
|
8521
8660
|
var MAX_SESSION_NODES = 256;
|
|
8522
8661
|
var MAX_QUERY_CANDIDATES = 80;
|
|
8523
8662
|
var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
8663
|
+
var CONTEXT_GRAPH_SCHEMA_VERSION = 1;
|
|
8524
8664
|
function nodeSqliteAdapter(db) {
|
|
8525
8665
|
return {
|
|
8526
8666
|
exec: (sql) => db.exec(sql),
|
|
@@ -8543,7 +8683,7 @@ function nodeSqliteAdapter(db) {
|
|
|
8543
8683
|
}
|
|
8544
8684
|
function openDatabase() {
|
|
8545
8685
|
const fp = contextGraphFile();
|
|
8546
|
-
|
|
8686
|
+
fs8.mkdirSync(path11.dirname(fp), { recursive: true });
|
|
8547
8687
|
let db;
|
|
8548
8688
|
if ("bun" in process.versions) {
|
|
8549
8689
|
const { Database } = require2("bun:sqlite");
|
|
@@ -8553,7 +8693,7 @@ function openDatabase() {
|
|
|
8553
8693
|
db = nodeSqliteAdapter(new DatabaseSync(fp));
|
|
8554
8694
|
}
|
|
8555
8695
|
try {
|
|
8556
|
-
|
|
8696
|
+
fs8.chmodSync(fp, 384);
|
|
8557
8697
|
} catch {}
|
|
8558
8698
|
db.exec("PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;");
|
|
8559
8699
|
db.exec(`
|
|
@@ -8573,8 +8713,6 @@ function openDatabase() {
|
|
|
8573
8713
|
created_at INTEGER NOT NULL,
|
|
8574
8714
|
updated_at INTEGER NOT NULL
|
|
8575
8715
|
);
|
|
8576
|
-
CREATE UNIQUE INDEX IF NOT EXISTS context_nodes_fact
|
|
8577
|
-
ON context_nodes(project_id, session_id, kind, fact_key);
|
|
8578
8716
|
CREATE INDEX IF NOT EXISTS context_nodes_project_status
|
|
8579
8717
|
ON context_nodes(project_id, status, updated_at DESC);
|
|
8580
8718
|
CREATE TABLE IF NOT EXISTS context_edges (
|
|
@@ -8592,6 +8730,20 @@ function openDatabase() {
|
|
|
8592
8730
|
tokenize='unicode61 remove_diacritics 2'
|
|
8593
8731
|
);
|
|
8594
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
|
+
}
|
|
8595
8747
|
return db;
|
|
8596
8748
|
}
|
|
8597
8749
|
function stableId(...parts) {
|
|
@@ -8641,7 +8793,7 @@ function makeNode(scope, kind, title, content, options = {}) {
|
|
|
8641
8793
|
const key = factKey(content);
|
|
8642
8794
|
const now = Date.now();
|
|
8643
8795
|
return {
|
|
8644
|
-
id: stableId(scope.projectId, scope.sessionId, kind, key),
|
|
8796
|
+
id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
|
|
8645
8797
|
projectId: scope.projectId,
|
|
8646
8798
|
sessionId: scope.sessionId,
|
|
8647
8799
|
branchHeadId: scope.branchHeadId ?? null,
|
|
@@ -8668,27 +8820,55 @@ function ensureFileNode(db, scope, file, now, content = file) {
|
|
|
8668
8820
|
upsertNode(db, node);
|
|
8669
8821
|
return node;
|
|
8670
8822
|
}
|
|
8671
|
-
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));
|
|
8672
8828
|
const rows = db.query(`
|
|
8673
|
-
SELECT
|
|
8674
|
-
WHERE project_id = ? AND session_id = ? AND
|
|
8675
|
-
|
|
8676
|
-
|
|
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)
|
|
8677
8842
|
return;
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
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));
|
|
8685
8863
|
}
|
|
8686
8864
|
function addFact(db, scope, sessionNodeId, kind, title, content, relatedPaths = [], confidence = 0.85, keyText = content) {
|
|
8687
8865
|
if (!content.trim())
|
|
8688
8866
|
return;
|
|
8689
8867
|
const node = makeNode(scope, kind, title, content, { relatedPaths, confidence });
|
|
8690
8868
|
node.factKey = factKey(keyText);
|
|
8691
|
-
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;
|
|
8692
8872
|
upsertNode(db, node);
|
|
8693
8873
|
linkNodes(db, scope.projectId, sessionNodeId, node.id, "contains", 1, node.updatedAt);
|
|
8694
8874
|
for (const file of relatedPaths) {
|
|
@@ -8729,7 +8909,8 @@ function indexCompactionState(projectId, state) {
|
|
|
8729
8909
|
const scope = {
|
|
8730
8910
|
projectId,
|
|
8731
8911
|
sessionId,
|
|
8732
|
-
branchHeadId: state.scope.branchHeadId
|
|
8912
|
+
branchHeadId: state.scope.branchHeadId,
|
|
8913
|
+
branchEntryIds: state.scope.branchAncestryIds
|
|
8733
8914
|
};
|
|
8734
8915
|
let db = null;
|
|
8735
8916
|
try {
|
|
@@ -8745,17 +8926,15 @@ function indexCompactionState(projectId, state) {
|
|
|
8745
8926
|
upsertNode(db, projectNode);
|
|
8746
8927
|
upsertNode(db, sessionNode);
|
|
8747
8928
|
linkNodes(db, projectId, projectNode.id, sessionNode.id, "contains", 1, now);
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
)
|
|
8756
|
-
`).run(projectId, sessionId);
|
|
8757
|
-
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
|
+
}
|
|
8758
8936
|
addFact(db, scope, sessionNode.id, "goal", "Current goal", state.goal, [], 0.98);
|
|
8937
|
+
}
|
|
8759
8938
|
for (const item of state.decisions) {
|
|
8760
8939
|
addFact(db, scope, sessionNode.id, "decision", "Decision", item.summary + (item.userResponse ? " \u2192 " + item.userResponse : ""), [], item.type === "explicit" ? 0.98 : 0.82, item.summary);
|
|
8761
8940
|
}
|
|
@@ -8774,6 +8953,8 @@ function indexCompactionState(projectId, state) {
|
|
|
8774
8953
|
relatedPaths: item.files,
|
|
8775
8954
|
confidence: item.priority === "critical" || item.priority === "high" ? 0.98 : 0.88
|
|
8776
8955
|
});
|
|
8956
|
+
if (sameActiveFact(latestLineageFact(db, scope, "loop", node.factKey), node))
|
|
8957
|
+
continue;
|
|
8777
8958
|
upsertNode(db, node);
|
|
8778
8959
|
linkNodes(db, projectId, sessionNode.id, node.id, "contains", 1, now);
|
|
8779
8960
|
for (const file of item.files) {
|
|
@@ -8894,13 +9075,17 @@ function saveContextMemory(scope, memory) {
|
|
|
8894
9075
|
node.sessionId = "*";
|
|
8895
9076
|
node.branchHeadId = null;
|
|
8896
9077
|
const transaction = db.transaction(() => {
|
|
8897
|
-
const
|
|
9078
|
+
const existing = db.query("SELECT status FROM context_nodes WHERE id = ?").get(node.id);
|
|
8898
9079
|
const duplicates = db.query(`
|
|
8899
|
-
SELECT id, related_paths FROM context_nodes
|
|
9080
|
+
SELECT id, related_paths, status FROM context_nodes
|
|
8900
9081
|
WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND id <> ?
|
|
8901
9082
|
`).all(scope.projectId, memory.kind, node.factKey, node.id);
|
|
8902
|
-
const count = db.query(
|
|
8903
|
-
|
|
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) {
|
|
8904
9089
|
throw new Error("Project memory limit reached; resolve an existing memory before saving another");
|
|
8905
9090
|
}
|
|
8906
9091
|
node.relatedPaths = Array.from(new Set([
|
|
@@ -8987,6 +9172,19 @@ function parsePaths(value) {
|
|
|
8987
9172
|
return [];
|
|
8988
9173
|
}
|
|
8989
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
|
+
}
|
|
8990
9188
|
function recallContext(scope, query, options = {}) {
|
|
8991
9189
|
const terms = searchTerms(query.slice(0, 500));
|
|
8992
9190
|
if (!terms.length)
|
|
@@ -9009,7 +9207,8 @@ function recallContext(scope, query, options = {}) {
|
|
|
9009
9207
|
candidates.set(neighbor.row.id, { row: neighbor.row, lexical: 0, graph: neighbor.weight });
|
|
9010
9208
|
}
|
|
9011
9209
|
const allowedKinds = options.kinds?.length ? new Set(options.kinds) : null;
|
|
9012
|
-
const branchIds = new Set(scope
|
|
9210
|
+
const branchIds = new Set(branchLineage(scope));
|
|
9211
|
+
const latestVersions = latestLineageVersions(db, scope);
|
|
9013
9212
|
const kindBoost = {
|
|
9014
9213
|
decision: 0.1,
|
|
9015
9214
|
constraint: 0.1,
|
|
@@ -9028,6 +9227,11 @@ function recallContext(scope, query, options = {}) {
|
|
|
9028
9227
|
return [];
|
|
9029
9228
|
if (allowedKinds && !allowedKinds.has(row.kind))
|
|
9030
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
|
+
}
|
|
9031
9235
|
const recency = Math.max(0, 1 - (now - row.updated_at) / NINETY_DAYS_MS);
|
|
9032
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));
|
|
9033
9237
|
return [{ row, score, sameSession, sameBranch }];
|
|
@@ -9750,8 +9954,8 @@ function createCompactionCommitStore(options = {}) {
|
|
|
9750
9954
|
|
|
9751
9955
|
// src/app/native-continuity-bridge.ts
|
|
9752
9956
|
import crypto6 from "crypto";
|
|
9753
|
-
import
|
|
9754
|
-
import
|
|
9957
|
+
import fs9 from "fs";
|
|
9958
|
+
import path12 from "path";
|
|
9755
9959
|
var MAX_TEXT_BYTES = 256 * 1024;
|
|
9756
9960
|
function sameScope(a, b) {
|
|
9757
9961
|
return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
|
|
@@ -9761,14 +9965,14 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9761
9965
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
9762
9966
|
const now = opts.now ?? Date.now;
|
|
9763
9967
|
const dir = opts.dir ?? nativeContinuityDir();
|
|
9764
|
-
const lockTarget =
|
|
9765
|
-
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");
|
|
9766
9970
|
const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
|
|
9767
9971
|
const readEntry = (file) => {
|
|
9768
9972
|
try {
|
|
9769
|
-
if (
|
|
9973
|
+
if (fs9.statSync(file).size > MAX_TEXT_BYTES * 2)
|
|
9770
9974
|
return null;
|
|
9771
|
-
const value = JSON.parse(
|
|
9975
|
+
const value = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
9772
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))
|
|
9773
9977
|
return null;
|
|
9774
9978
|
return value;
|
|
@@ -9780,16 +9984,16 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9780
9984
|
const fresh = [];
|
|
9781
9985
|
let files = [];
|
|
9782
9986
|
try {
|
|
9783
|
-
files =
|
|
9987
|
+
files = fs9.readdirSync(dir).filter((file) => file.endsWith(".json"));
|
|
9784
9988
|
} catch {
|
|
9785
9989
|
return fresh;
|
|
9786
9990
|
}
|
|
9787
9991
|
for (const name of files) {
|
|
9788
|
-
const file =
|
|
9992
|
+
const file = path12.join(dir, name);
|
|
9789
9993
|
const entry = readEntry(file);
|
|
9790
9994
|
if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
|
|
9791
9995
|
try {
|
|
9792
|
-
|
|
9996
|
+
fs9.unlinkSync(file);
|
|
9793
9997
|
} catch {}
|
|
9794
9998
|
} else {
|
|
9795
9999
|
fresh.push({ file, entry });
|
|
@@ -9800,7 +10004,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9800
10004
|
const oldest = fresh.shift();
|
|
9801
10005
|
if (oldest)
|
|
9802
10006
|
try {
|
|
9803
|
-
|
|
10007
|
+
fs9.unlinkSync(oldest.file);
|
|
9804
10008
|
} catch {}
|
|
9805
10009
|
}
|
|
9806
10010
|
return fresh;
|
|
@@ -9808,7 +10012,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9808
10012
|
const locked = (work) => {
|
|
9809
10013
|
ensureDir(dir);
|
|
9810
10014
|
try {
|
|
9811
|
-
|
|
10015
|
+
fs9.chmodSync(dir, 448);
|
|
9812
10016
|
} catch {}
|
|
9813
10017
|
const release = acquireLockSync(lockTarget);
|
|
9814
10018
|
try {
|
|
@@ -9825,13 +10029,13 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9825
10029
|
locked(() => {
|
|
9826
10030
|
const target = fileFor(scope);
|
|
9827
10031
|
try {
|
|
9828
|
-
|
|
10032
|
+
fs9.unlinkSync(target);
|
|
9829
10033
|
} catch {}
|
|
9830
10034
|
prune(1);
|
|
9831
10035
|
const entry = { schemaVersion: 1, scope, text, createdAt: now() };
|
|
9832
10036
|
atomicWriteFileSync(target, JSON.stringify(entry));
|
|
9833
10037
|
try {
|
|
9834
|
-
|
|
10038
|
+
fs9.chmodSync(target, 384);
|
|
9835
10039
|
} catch {}
|
|
9836
10040
|
});
|
|
9837
10041
|
} catch (error2) {
|
|
@@ -9849,7 +10053,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9849
10053
|
if (!entry)
|
|
9850
10054
|
return null;
|
|
9851
10055
|
try {
|
|
9852
|
-
|
|
10056
|
+
fs9.unlinkSync(target);
|
|
9853
10057
|
} catch {
|
|
9854
10058
|
return null;
|
|
9855
10059
|
}
|
|
@@ -9865,13 +10069,13 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
9865
10069
|
locked(() => {
|
|
9866
10070
|
if (scope) {
|
|
9867
10071
|
try {
|
|
9868
|
-
|
|
10072
|
+
fs9.unlinkSync(fileFor(scope));
|
|
9869
10073
|
} catch {}
|
|
9870
10074
|
return;
|
|
9871
10075
|
}
|
|
9872
10076
|
for (const item of prune(0)) {
|
|
9873
10077
|
try {
|
|
9874
|
-
|
|
10078
|
+
fs9.unlinkSync(item.file);
|
|
9875
10079
|
} catch {}
|
|
9876
10080
|
}
|
|
9877
10081
|
});
|
|
@@ -9932,15 +10136,16 @@ function resolveModels(ctx, primary, config, explicit = false) {
|
|
|
9932
10136
|
return { segModel, sumModel, verifyModel };
|
|
9933
10137
|
}
|
|
9934
10138
|
function resolveGraphScope(ctx) {
|
|
10139
|
+
const projectId = deriveProjectIdFromCwd(ctx.cwd);
|
|
9935
10140
|
const sessionId = resolveSessionId(ctx);
|
|
9936
|
-
if (isUnresolvedSessionId(sessionId))
|
|
10141
|
+
if (!projectId || isUnresolvedSessionId(sessionId))
|
|
9937
10142
|
return null;
|
|
9938
|
-
const
|
|
10143
|
+
const ancestryIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
9939
10144
|
return {
|
|
9940
|
-
projectId
|
|
10145
|
+
projectId,
|
|
9941
10146
|
sessionId,
|
|
9942
|
-
branchHeadId:
|
|
9943
|
-
branchEntryIds
|
|
10147
|
+
branchHeadId: ancestryIds.at(-1),
|
|
10148
|
+
branchEntryIds: ancestryIds
|
|
9944
10149
|
};
|
|
9945
10150
|
}
|
|
9946
10151
|
function smartCompactExtension(pi) {
|
|
@@ -10022,7 +10227,7 @@ function smartCompactExtension(pi) {
|
|
|
10022
10227
|
}
|
|
10023
10228
|
const scope = resolveGraphScope(ctx);
|
|
10024
10229
|
if (!scope)
|
|
10025
|
-
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 };
|
|
10026
10231
|
const results = recallContext(scope, params.query, {
|
|
10027
10232
|
limit: params.limit,
|
|
10028
10233
|
sessionOnly: params.scope === "session",
|
|
@@ -10056,11 +10261,11 @@ function smartCompactExtension(pi) {
|
|
|
10056
10261
|
}
|
|
10057
10262
|
const scope = resolveGraphScope(ctx);
|
|
10058
10263
|
if (!scope)
|
|
10059
|
-
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 };
|
|
10060
10265
|
const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
|
|
10061
10266
|
const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
|
|
10062
10267
|
const content = scrubber.scrubText(params.content).value;
|
|
10063
|
-
const relatedPaths = (params.related_paths ?? []).map((
|
|
10268
|
+
const relatedPaths = (params.related_paths ?? []).map((path13) => scrubber.scrubText(path13).value);
|
|
10064
10269
|
const status = params.status ?? "active";
|
|
10065
10270
|
if (!ctx.hasUI) {
|
|
10066
10271
|
return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
|
|
@@ -10068,7 +10273,7 @@ function smartCompactExtension(pi) {
|
|
|
10068
10273
|
const approved = await ctx.ui.confirm(status === "resolved" ? "Resolve Project Memory" : "Save Project Memory", "Kind: " + params.kind + `
|
|
10069
10274
|
Title: ` + title + `
|
|
10070
10275
|
|
|
10071
|
-
` + content
|
|
10276
|
+
` + content + (relatedPaths.length ? `
|
|
10072
10277
|
|
|
10073
10278
|
Paths: ` + relatedPaths.join(", ") : ""));
|
|
10074
10279
|
if (!approved || signal?.aborted) {
|
|
@@ -10109,16 +10314,16 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10109
10314
|
const maxLlmCalls = maxCallsRaw == null ? undefined : Number(maxCallsRaw);
|
|
10110
10315
|
const maxLlmInputTokens = maxInputRaw == null ? undefined : Number(maxInputRaw);
|
|
10111
10316
|
const maxLatencyMs = maxLatencyRaw == null ? undefined : Number(maxLatencyRaw);
|
|
10112
|
-
if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls <
|
|
10113
|
-
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");
|
|
10114
10319
|
return;
|
|
10115
10320
|
}
|
|
10116
|
-
if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens <
|
|
10117
|
-
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");
|
|
10118
10323
|
return;
|
|
10119
10324
|
}
|
|
10120
|
-
if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs <
|
|
10121
|
-
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");
|
|
10122
10327
|
return;
|
|
10123
10328
|
}
|
|
10124
10329
|
if (flags.includes("metrics") || flags.includes("dashboard")) {
|
|
@@ -10186,8 +10391,12 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10186
10391
|
}
|
|
10187
10392
|
if (flags.includes("loops")) {
|
|
10188
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
|
+
}
|
|
10189
10398
|
const sessionId = resolveSessionId(ctx);
|
|
10190
|
-
const branchIds = ctx.sessionManager.getBranch()
|
|
10399
|
+
const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
10191
10400
|
const state = isUnresolvedSessionId(sessionId) ? null : loadScopedCompactionState({ projectId, sessionId }, branchIds);
|
|
10192
10401
|
if (!state || state.openLoops.length === 0) {
|
|
10193
10402
|
ctx.ui.notify("No persisted open loops for this project", "info");
|
|
@@ -10368,7 +10577,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10368
10577
|
if (isUnresolvedSessionId(sessionId))
|
|
10369
10578
|
return;
|
|
10370
10579
|
const projectId = deriveProjectIdFromCwd(ctx.cwd);
|
|
10371
|
-
|
|
10580
|
+
if (!projectId)
|
|
10581
|
+
return;
|
|
10582
|
+
const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
|
|
10372
10583
|
const branchHeadId = typeof event.compactionEntry.id === "string" ? event.compactionEntry.id : branchIds.at(-1);
|
|
10373
10584
|
if (!branchHeadId)
|
|
10374
10585
|
return;
|
|
@@ -10444,9 +10655,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10444
10655
|
report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
|
|
10445
10656
|
dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." },
|
|
10446
10657
|
focus: { type: "string", description: "Topic or path that should receive extra preservation budget." },
|
|
10447
|
-
max_calls: { type: "number", description: "Maximum LLM calls for this run (
|
|
10448
|
-
max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (
|
|
10449
|
-
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 + ")." }
|
|
10450
10661
|
}
|
|
10451
10662
|
},
|
|
10452
10663
|
async execute(_id, params, signal, _onUp, ctx) {
|
|
@@ -10455,9 +10666,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
10455
10666
|
const verbose = !!params.verbose;
|
|
10456
10667
|
const dryRun = !!params.dry_run;
|
|
10457
10668
|
const focus = typeof params.focus === "string" ? params.focus.trim() || undefined : undefined;
|
|
10458
|
-
const maxLlmCalls = typeof params.max_calls === "number" && Number.isInteger(params.max_calls) && params.max_calls >=
|
|
10459
|
-
const maxLlmInputTokens = typeof params.max_input_tokens === "number" && Number.isInteger(params.max_input_tokens) && params.max_input_tokens >=
|
|
10460
|
-
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;
|
|
10461
10672
|
if (params.report || params.dashboard) {
|
|
10462
10673
|
const report = buildMetricsReport();
|
|
10463
10674
|
const fp = params.dashboard ? writeMetricsDashboard() : null;
|