pi-smart-compact 8.0.6 → 8.0.8

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.
Files changed (47) hide show
  1. package/ARCHITECTURE.md +513 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +70 -51
  4. package/SECURITY.md +8 -3
  5. package/dist/app/mode-policy.d.ts +0 -1
  6. package/dist/app/mode-policy.d.ts.map +1 -1
  7. package/dist/app/preflight.d.ts +16 -2
  8. package/dist/app/preflight.d.ts.map +1 -1
  9. package/dist/app/steps/extract.d.ts.map +1 -1
  10. package/dist/app/steps/state.d.ts.map +1 -1
  11. package/dist/app/steps/synthesize.d.ts.map +1 -1
  12. package/dist/app/steps/window.d.ts +3 -1
  13. package/dist/app/steps/window.d.ts.map +1 -1
  14. package/dist/constants.d.ts +18 -1
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/domain/telemetry.d.ts +3 -0
  17. package/dist/domain/telemetry.d.ts.map +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +762 -549
  20. package/dist/infra/context-graph.d.ts.map +1 -1
  21. package/dist/infra/fs.d.ts +1 -1
  22. package/dist/infra/fs.d.ts.map +1 -1
  23. package/dist/infra/git.d.ts.map +1 -1
  24. package/dist/infra/session-identity.d.ts +4 -0
  25. package/dist/infra/session-identity.d.ts.map +1 -1
  26. package/dist/phases/verify.d.ts.map +1 -1
  27. package/dist/provider-eval.js +113 -22
  28. package/dist/provider-scenario-eval.js +153 -111
  29. package/dist/telemetry-report.js +138 -41
  30. package/dist/ui/dashboard-insights.d.ts.map +1 -1
  31. package/dist/ui/overlays.d.ts.map +1 -1
  32. package/dist/utils/cache.d.ts.map +1 -1
  33. package/dist/utils/extraction.d.ts +5 -0
  34. package/dist/utils/extraction.d.ts.map +1 -1
  35. package/dist/utils/file-needles.d.ts +7 -0
  36. package/dist/utils/file-needles.d.ts.map +1 -1
  37. package/dist/utils/fingerprint.d.ts +1 -1
  38. package/dist/utils/fingerprint.d.ts.map +1 -1
  39. package/dist/utils/pruning.d.ts.map +1 -1
  40. package/dist/utils/state.d.ts +4 -3
  41. package/dist/utils/state.d.ts.map +1 -1
  42. package/dist/utils/tokens.d.ts.map +1 -1
  43. package/docs/MIGRATING_TO_V8.md +30 -13
  44. package/docs/RELEASE.md +13 -10
  45. package/package.json +2 -1
  46. package/dist/app/explore-wrap.d.ts +0 -8
  47. 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.6";
105
+ var VERSION = "8.0.8";
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
- try {
484
- fs.mkdirSync(dir, { recursive: true });
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.get(exactKey);
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.get(key) ?? 1;
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.delete(key);
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
- var DELETE_RESULT_RE = /\b(?:deleted|removed|unlinked)\b/i;
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 syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
1296
- idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
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
- if (DELETE_RESULT_RE.test(extractText(m.content)))
1329
- delSet.add(filePath);
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 (const m of msgs) {
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 txt = extractText(m.content).trim();
1558
- if (txt && !txt.startsWith("/"))
1559
- return txt.slice(0, TRUNC.MESSAGE);
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: [...new Set([...base.readFiles, ...delta.readFiles])],
3086
- deletedFiles: [...new Set([...base.deletedFiles, ...delta.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: base.mainGoal ?? delta.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 successes = entries.filter((entry) => entry.status === "success" || entry.status === "dry-run");
3216
- const quality = successes.filter((entry) => typeof entry.verificationScore === "number");
3217
- const appliedRuns = entries.filter((entry) => entry.status === "success");
3218
- const appliedRunIds = new Set(appliedRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
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
- successRate: entries.length ? successes.length / entries.length : 0,
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: entries.length ? quality.length / entries.length : 0,
3231
- p95LatencyMs: p95(entries.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
3232
- avgTokens: entries.length ? entries.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / entries.length : 0,
3233
- fallbackRate: entries.length ? entries.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / entries.length : 0,
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: appliedRuns.length ? observedScores.size / appliedRuns.length : 0
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.runs >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
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 dataConfidence = Math.round(100 * (Math.min(1, canary.runs / minCanaryRuns) * 0.25 + Math.min(1, baseline.runs / Math.max(20, minCanaryRuns)) * 0.15 + canary.qualityCoverage * 0.2 + canary.damageCoverage * 0.2 + baseline.damageCoverage * 0.2));
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.runs >= 3) {
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.runs < minCanaryRuns) {
3295
- reasons.push("need " + (minCanaryRuns - canary.runs) + " more canary runs");
3296
- } else if (baseline.runs < Math.max(20, minCanaryRuns)) {
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 FAILURE_KINDS = new Set([
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 (entry.failureKind && knownFailures.has(entry.failureKind)) {
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 n=" + canary.baseline.runs + " \xB7 canary n=" + canary.canary.runs,
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", { cwd, encoding: "utf-8", timeout: 2000 });
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
- return hashProjectId(findGitRoot2(cwd) ?? cwd);
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 existing = loadProjectFingerprint(projectId);
4355
- const newKnownFiles = [...new Set([
4356
- ...existing?.knownFiles ?? [],
4357
- ...extraction.modifiedFiles.map((f) => f.path),
4358
- ...extraction.readFiles
4359
- ])].slice(-50);
4360
- const detectedLanguage = detectLanguage(extraction);
4361
- const detectedFramework = detectFramework(extraction);
4362
- const fingerprint = {
4363
- id: projectId,
4364
- language: existing?.language && existing.language !== "unknown" ? existing.language : detectedLanguage,
4365
- framework: existing?.framework ?? detectedFramework,
4366
- keyDirectories: extractKeyDirs(extraction),
4367
- knownFiles: newKnownFiles,
4368
- sessionCount: (existing?.sessionCount ?? 0) + 1,
4369
- updatedAt: Date.now()
4370
- };
4371
- writeJsonSync(getFingerprintPath(projectId), fingerprint, true);
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 targetRetainedTokens = modelContextWindow ? Math.max(0, modelContextWindow * targetPercent / 100 - fixedContextTokens - profileCfg.summaryBudgetTokens) : adaptiveKeepTokens;
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
- const detail = plan.reason === "insufficient-projected-saving" ? "projected saving is below 10%." : plan.reason === "unsafe-tool-boundary" ? "no safe tool-call boundary is available." : plan.reason === "no-eligible-prefix" ? "no eligible prefix remains." : plan.reason === "retention-target-exceeded" ? "a complete tool pair exceeds the retention target." : "the projected context remains above the mode target.";
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
- `).replace(/\n{3,}/g, `
4926
-
4927
- `).trim() + `
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 upsertSection(summary, kind, body, placement) {
4931
- const heading = canonicalHeading(kind);
4932
- const existing = summary.sections.findIndex((s) => s.kind === kind);
4933
- if (existing >= 0) {
4934
- const sections = summary.sections.slice();
4935
- sections[existing] = { kind, heading, body: body.trim() };
4936
- return { sections };
4937
- }
4938
- const hint = placement == null ? {} : typeof placement === "string" ? { before: placement } : placement;
4939
- const section = { kind, heading, body: body.trim() };
4940
- if (hint.before) {
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 { sections: [...summary.sections, section] };
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 appendToSection(summary, kind, text, fallbackBody = "") {
4965
- const heading = canonicalHeading(kind);
4966
- const idx = summary.sections.findIndex((s) => s.kind === kind);
4967
- if (idx >= 0) {
4968
- const sections = summary.sections.slice();
4969
- const existing = sections[idx];
4970
- const body = /^-\s*(?:none|none recorded|no blockers?|yok)[.!]?$/i.test(existing.body.trim()) ? "" : existing.body.trim();
4971
- const combined = body ? body + `
4972
- ` + text.trim() : text.trim();
4973
- sections[idx] = { kind, heading, body: combined };
4974
- return { sections };
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
- return upsertSection(summary, kind, (fallbackBody.trim() ? fallbackBody.trim() + `
4977
- ` : "") + text.trim());
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) => !isLegacySearchOutput(item.message));
4991
- const openLoops = state.openLoops.filter((item) => !isLegacySearchOutput(item.summary));
4992
- if (constraints.length === state.constraints.length && unresolvedErrors.length === state.unresolvedErrors.length && openLoops.length === state.openLoops.length)
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, branchEntryIds = []) {
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 && branchEntryIds.length > 0 && !branchEntryIds.includes(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
- return applyContinuityOverrides(current, current.factOverrides ?? []);
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 = mergeBy(activeCurrent.openLoops, activePrevious.openLoops.filter((loop) => loop.status !== "resolved"), (item) => normalizeFactKey(item.summary), 25).map((item, index) => ({ ...item, id: ID_PREFIX.OPEN_LOOP + (index + 1) }));
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 key = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
5202
- const prevDecisionTexts = new Set(prev.decisions.map((d) => key(d.summary)));
5203
- const currDecisionTexts = new Set(current.decisions.map((d) => key(d.summary)));
5204
- const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(key(d.summary))).map((d) => d.summary);
5205
- const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(key(d.summary))).map((d) => d.summary);
5206
- const prevLoopSummaries = new Map(prev.openLoops.filter((loop) => loop.status !== "resolved").map((l) => [key(l.summary), l]));
5207
- const currLoopKeys = new Set(current.openLoops.filter((loop) => loop.status !== "resolved").map((l) => key(l.summary)));
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(key(l.summary))).map((l) => l.summary);
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) => key(e.message)));
5220
- const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => key(e.message)));
5221
- const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(key(e.message))).map((e) => e.message);
5222
- const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(key(e.message))).map((e) => e.message);
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
- switch (reason) {
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 quality " + quality + " \xB7 0 gaps" : "Smart compact applied \u2713 \u2014 " + before.toLocaleString() + "t \u2192 planned ~" + planned.toLocaleString() + "t / ~" + after.toLocaleString() + "t applied estimate \xB7 saved " + saving + "% \xB7 quality " + quality + "/100 \xB7 0 gaps", "info");
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", " Quality: ") + theme.fg(scoreColor, details.qualityScore + "/100"), 0, 0));
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: score " + provenance.initialScore + " \u2192 deterministic " + provenance.deterministicPatched.length + (provenance.llmPatched ? " \u2192 LLM patch" : "") + " \u2192 " + provenance.finalScore + " (" + provenance.remainingGaps.length + " remaining)"), 0, 0));
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?", "Quality " + details.qualityScore + "/100 \xB7 " + details.gaps.length + " remaining gap(s) \xB7 " + details.tokensSaved.toLocaleString() + ` estimated tokens saved.
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 plans = new Map(PRIMARY_MODES.map((mode) => [mode, planManualPreflight(ctx, selectedModel.model, mode, calibration, opts.config, damageMedian)]));
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 = typeof tool.id === "string" ? tool.id : block.id ? block.id + "_" + toolIndex : "mtu_" + idx + "_" + toolIndex;
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 pruning = pruneRedundant(rc.llmMessages);
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
- const backupPath = backupConversation(rc.services.scrubber.scrubText(convText).value, rc.sessionId);
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 ? Array.from(manager.getBranch()) : rc.branch;
6562
- const branchEntryIds = fullBranch.map((entry) => entry.id).filter((id) => typeof id === "string");
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
- ...branchEntryIds.length ? {
6568
- branchHeadId: branchEntryIds[branchEntryIds.length - 1],
6569
- branchAncestryIds: branchEntryIds.slice(-256)
6666
+ ...ancestryIds.length ? {
6667
+ branchHeadId: ancestryIds[ancestryIds.length - 1],
6668
+ branchAncestryIds: ancestryIds
6570
6669
  } : {}
6571
6670
  };
6572
- const previousState = loadScopedCompactionState(continuityScope, branchEntryIds);
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.config.focusWeighting ? rc.focus : undefined,
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 mode selected " + refined + " from deterministic session risk", "info");
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 advance(hit, "_synthesized");
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 advance(deterministic, "_synthesized");
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 advance(out, "_synthesized");
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
- if (negative && !hasNearbyMarker(tokens, anchor, NEGATION_MARKERS)) {
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
- if (negative && !hasNearbyMarker(tokens, anchor, NEGATION_MARKERS)) {
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
  }
@@ -8008,9 +8136,11 @@ function verifySummary(summary, extraction, continuity = null) {
8008
8136
  }
8009
8137
  }
8010
8138
  const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
8139
+ const listedModifiedPaths = new Set((findSection(parsed, "files-modified")?.body ?? "").split(`
8140
+ `).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).map((line) => line.startsWith("`") && line.endsWith("`") ? line.slice(1, -1) : line).filter(Boolean).map(normalizePath));
8011
8141
  for (const file of extraction.modifiedFiles) {
8012
8142
  const needles = buildUniquePathNeedles(file.path, modifiedPaths);
8013
- if (!needles.some((needle) => lower.includes(needle))) {
8143
+ if (!listedModifiedPaths.has(normalizePath(file.path)) && !needles.some((needle) => lower.includes(needle))) {
8014
8144
  gaps.push({ kind: "missing-file", path: file.path });
8015
8145
  score -= 5;
8016
8146
  }
@@ -8344,6 +8474,10 @@ async function verifyAndPatch(rc) {
8344
8474
  return advance(out, "_verified");
8345
8475
  }
8346
8476
 
8477
+ // src/app/steps/state.ts
8478
+ import fs7 from "fs";
8479
+ import path10 from "path";
8480
+
8347
8481
  // src/domain/yield-gate.ts
8348
8482
  class YieldGateError extends Error {
8349
8483
  reason;
@@ -8424,9 +8558,14 @@ function buildState(rc) {
8424
8558
  const nextActions = extractNextActions(summary);
8425
8559
  const criticalContextItems = extractCriticalContext(summary);
8426
8560
  const currentState = buildCompactionState(extraction, managedLoops, rc.explorationReport, nextActions, criticalContextItems, loopOverrides);
8561
+ const summarizedGoal = summaryEvidenceLine(findSection(summary, "goal")?.body ?? "", TRUNC.MESSAGE);
8427
8562
  currentState.scope = rc.continuityScope;
8428
8563
  currentState.factOverrides = prevState?.factOverrides ?? [];
8429
8564
  let compactionState = mergeCompactionStates(prevState, currentState);
8565
+ compactionState.deletedFiles = compactionState.deletedFiles.filter((file) => {
8566
+ const candidate = path10.isAbsolute(file) ? file : path10.resolve(rc.ctx.cwd, file);
8567
+ return !fs7.existsSync(candidate);
8568
+ });
8430
8569
  if (preserve.length > 0) {
8431
8570
  compactionState.readFiles = Array.from(new Set([...preserve, ...compactionState.readFiles])).slice(0, 100);
8432
8571
  }
@@ -8439,6 +8578,8 @@ function buildState(rc) {
8439
8578
  rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
8440
8579
  }
8441
8580
  }
8581
+ if (summarizedGoal && currentState.goal)
8582
+ compactionState.goal = summarizedGoal;
8442
8583
  const continuity = renderContinuityCapsule(compactionState, undefined, summary);
8443
8584
  if (continuity)
8444
8585
  summary = summary.trimEnd() + `
@@ -8512,8 +8653,8 @@ function buildState(rc) {
8512
8653
 
8513
8654
  // src/infra/context-graph.ts
8514
8655
  import { createHash as createHash3 } from "crypto";
8515
- import fs7 from "fs";
8516
- import path10 from "path";
8656
+ import fs8 from "fs";
8657
+ import path11 from "path";
8517
8658
  import { createRequire } from "module";
8518
8659
  var require2 = createRequire(import.meta.url);
8519
8660
  var MAX_PROJECT_NODES = 2000;
@@ -8521,6 +8662,7 @@ var MAX_MANUAL_NODES = 500;
8521
8662
  var MAX_SESSION_NODES = 256;
8522
8663
  var MAX_QUERY_CANDIDATES = 80;
8523
8664
  var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
8665
+ var CONTEXT_GRAPH_SCHEMA_VERSION = 1;
8524
8666
  function nodeSqliteAdapter(db) {
8525
8667
  return {
8526
8668
  exec: (sql) => db.exec(sql),
@@ -8543,7 +8685,7 @@ function nodeSqliteAdapter(db) {
8543
8685
  }
8544
8686
  function openDatabase() {
8545
8687
  const fp = contextGraphFile();
8546
- fs7.mkdirSync(path10.dirname(fp), { recursive: true });
8688
+ fs8.mkdirSync(path11.dirname(fp), { recursive: true });
8547
8689
  let db;
8548
8690
  if ("bun" in process.versions) {
8549
8691
  const { Database } = require2("bun:sqlite");
@@ -8553,7 +8695,7 @@ function openDatabase() {
8553
8695
  db = nodeSqliteAdapter(new DatabaseSync(fp));
8554
8696
  }
8555
8697
  try {
8556
- fs7.chmodSync(fp, 384);
8698
+ fs8.chmodSync(fp, 384);
8557
8699
  } catch {}
8558
8700
  db.exec("PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;");
8559
8701
  db.exec(`
@@ -8573,8 +8715,6 @@ function openDatabase() {
8573
8715
  created_at INTEGER NOT NULL,
8574
8716
  updated_at INTEGER NOT NULL
8575
8717
  );
8576
- CREATE UNIQUE INDEX IF NOT EXISTS context_nodes_fact
8577
- ON context_nodes(project_id, session_id, kind, fact_key);
8578
8718
  CREATE INDEX IF NOT EXISTS context_nodes_project_status
8579
8719
  ON context_nodes(project_id, status, updated_at DESC);
8580
8720
  CREATE TABLE IF NOT EXISTS context_edges (
@@ -8592,6 +8732,20 @@ function openDatabase() {
8592
8732
  tokenize='unicode61 remove_diacritics 2'
8593
8733
  );
8594
8734
  `);
8735
+ const version = db.query("PRAGMA user_version").get();
8736
+ if (Number(version?.user_version ?? 0) < CONTEXT_GRAPH_SCHEMA_VERSION) {
8737
+ db.transaction(() => {
8738
+ db.exec(`
8739
+ DROP INDEX IF EXISTS context_nodes_fact;
8740
+ DELETE FROM context_nodes_fts
8741
+ WHERE node_id IN (SELECT id FROM context_nodes WHERE source = 'compaction');
8742
+ DELETE FROM context_nodes WHERE source = 'compaction';
8743
+ CREATE UNIQUE INDEX context_nodes_fact
8744
+ ON context_nodes(project_id, session_id, kind, fact_key, COALESCE(branch_head_id, ''));
8745
+ PRAGMA user_version = ${CONTEXT_GRAPH_SCHEMA_VERSION};
8746
+ `);
8747
+ })();
8748
+ }
8595
8749
  return db;
8596
8750
  }
8597
8751
  function stableId(...parts) {
@@ -8641,7 +8795,7 @@ function makeNode(scope, kind, title, content, options = {}) {
8641
8795
  const key = factKey(content);
8642
8796
  const now = Date.now();
8643
8797
  return {
8644
- id: stableId(scope.projectId, scope.sessionId, kind, key),
8798
+ id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
8645
8799
  projectId: scope.projectId,
8646
8800
  sessionId: scope.sessionId,
8647
8801
  branchHeadId: scope.branchHeadId ?? null,
@@ -8668,27 +8822,55 @@ function ensureFileNode(db, scope, file, now, content = file) {
8668
8822
  upsertNode(db, node);
8669
8823
  return node;
8670
8824
  }
8671
- function markFactStatus(db, scope, kind, key, status) {
8825
+ function branchLineage(scope) {
8826
+ return Array.from(new Set([...scope.branchEntryIds ?? [], scope.branchHeadId].filter((id) => typeof id === "string" && id.length > 0)));
8827
+ }
8828
+ function lineageFactRows(db, scope, kind, key) {
8829
+ const lineage = new Set(branchLineage(scope));
8672
8830
  const rows = db.query(`
8673
- SELECT id FROM context_nodes
8674
- WHERE project_id = ? AND session_id = ? AND kind = ? AND fact_key = ? AND source = 'compaction'
8675
- `).all(scope.projectId, scope.sessionId, kind, key);
8676
- if (!rows.length)
8831
+ SELECT * FROM context_nodes
8832
+ WHERE project_id = ? AND session_id = ? AND source = 'compaction'
8833
+ ${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
8834
+ `).all(scope.projectId, scope.sessionId, ...kind ? [kind] : [], ...key ? [key] : []);
8835
+ return rows.filter((row) => lineage.size > 0 ? Boolean(row.branch_head_id && lineage.has(row.branch_head_id)) : row.branch_head_id == null);
8836
+ }
8837
+ function latestLineageFact(db, scope, kind, key) {
8838
+ const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
8839
+ 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;
8840
+ }
8841
+ function markFactStatus(db, scope, kind, key, status) {
8842
+ const previous = latestLineageFact(db, scope, kind, key);
8843
+ if (!previous || previous.status === status)
8677
8844
  return;
8678
- db.query(`
8679
- UPDATE context_nodes SET status = ?, updated_at = ?
8680
- WHERE project_id = ? AND session_id = ? AND kind = ? AND fact_key = ? AND source = 'compaction'
8681
- `).run(status, Date.now(), scope.projectId, scope.sessionId, kind, key);
8682
- const remove = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
8683
- for (const row of rows)
8684
- remove.run(row.id);
8845
+ const now = Date.now();
8846
+ upsertNode(db, {
8847
+ id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
8848
+ projectId: scope.projectId,
8849
+ sessionId: scope.sessionId,
8850
+ branchHeadId: scope.branchHeadId ?? null,
8851
+ kind,
8852
+ factKey: key,
8853
+ title: previous.title,
8854
+ content: previous.content,
8855
+ status,
8856
+ source: "compaction",
8857
+ confidence: previous.confidence,
8858
+ relatedPaths: parsePaths(previous.related_paths),
8859
+ createdAt: now,
8860
+ updatedAt: now
8861
+ }, true);
8862
+ }
8863
+ function sameActiveFact(row, node) {
8864
+ 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
8865
  }
8686
8866
  function addFact(db, scope, sessionNodeId, kind, title, content, relatedPaths = [], confidence = 0.85, keyText = content) {
8687
8867
  if (!content.trim())
8688
8868
  return;
8689
8869
  const node = makeNode(scope, kind, title, content, { relatedPaths, confidence });
8690
8870
  node.factKey = factKey(keyText);
8691
- node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey);
8871
+ node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey, scope.branchHeadId ?? "");
8872
+ if (sameActiveFact(latestLineageFact(db, scope, kind, node.factKey), node))
8873
+ return;
8692
8874
  upsertNode(db, node);
8693
8875
  linkNodes(db, scope.projectId, sessionNodeId, node.id, "contains", 1, node.updatedAt);
8694
8876
  for (const file of relatedPaths) {
@@ -8729,7 +8911,8 @@ function indexCompactionState(projectId, state) {
8729
8911
  const scope = {
8730
8912
  projectId,
8731
8913
  sessionId,
8732
- branchHeadId: state.scope.branchHeadId
8914
+ branchHeadId: state.scope.branchHeadId,
8915
+ branchEntryIds: state.scope.branchAncestryIds
8733
8916
  };
8734
8917
  let db = null;
8735
8918
  try {
@@ -8745,17 +8928,15 @@ function indexCompactionState(projectId, state) {
8745
8928
  upsertNode(db, projectNode);
8746
8929
  upsertNode(db, sessionNode);
8747
8930
  linkNodes(db, projectId, projectNode.id, sessionNode.id, "contains", 1, now);
8748
- db.query(`
8749
- UPDATE context_nodes SET status = 'superseded', updated_at = ?
8750
- WHERE project_id = ? AND session_id = ? AND kind = 'goal' AND source = 'compaction' AND status = 'active'
8751
- `).run(now, projectId, sessionId);
8752
- db.query(`
8753
- DELETE FROM context_nodes_fts WHERE node_id IN (
8754
- SELECT id FROM context_nodes WHERE project_id = ? AND session_id = ? AND kind = 'goal' AND status <> 'active'
8755
- )
8756
- `).run(projectId, sessionId);
8757
- if (state.goal)
8931
+ if (state.goal) {
8932
+ const currentGoalKey = factKey(state.goal);
8933
+ const priorGoalKeys = new Set(lineageFactRows(db, scope, "goal").map((row) => row.fact_key));
8934
+ for (const key of priorGoalKeys) {
8935
+ if (key !== currentGoalKey)
8936
+ markFactStatus(db, scope, "goal", key, "superseded");
8937
+ }
8758
8938
  addFact(db, scope, sessionNode.id, "goal", "Current goal", state.goal, [], 0.98);
8939
+ }
8759
8940
  for (const item of state.decisions) {
8760
8941
  addFact(db, scope, sessionNode.id, "decision", "Decision", item.summary + (item.userResponse ? " \u2192 " + item.userResponse : ""), [], item.type === "explicit" ? 0.98 : 0.82, item.summary);
8761
8942
  }
@@ -8774,6 +8955,8 @@ function indexCompactionState(projectId, state) {
8774
8955
  relatedPaths: item.files,
8775
8956
  confidence: item.priority === "critical" || item.priority === "high" ? 0.98 : 0.88
8776
8957
  });
8958
+ if (sameActiveFact(latestLineageFact(db, scope, "loop", node.factKey), node))
8959
+ continue;
8777
8960
  upsertNode(db, node);
8778
8961
  linkNodes(db, projectId, sessionNode.id, node.id, "contains", 1, now);
8779
8962
  for (const file of item.files) {
@@ -8894,13 +9077,17 @@ function saveContextMemory(scope, memory) {
8894
9077
  node.sessionId = "*";
8895
9078
  node.branchHeadId = null;
8896
9079
  const transaction = db.transaction(() => {
8897
- const exists = db.query("SELECT 1 AS found FROM context_nodes WHERE id = ?").get(node.id);
9080
+ const existing = db.query("SELECT status FROM context_nodes WHERE id = ?").get(node.id);
8898
9081
  const duplicates = db.query(`
8899
- SELECT id, related_paths FROM context_nodes
9082
+ SELECT id, related_paths, status FROM context_nodes
8900
9083
  WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND id <> ?
8901
9084
  `).all(scope.projectId, memory.kind, node.factKey, node.id);
8902
- const count = db.query("SELECT count(*) AS count FROM context_nodes WHERE project_id = ? AND source = 'manual'").get(scope.projectId);
8903
- if (!exists && !duplicates.length && Number(count?.count ?? 0) >= MAX_MANUAL_NODES) {
9085
+ const count = db.query(`
9086
+ SELECT count(*) AS count FROM context_nodes
9087
+ WHERE project_id = ? AND source = 'manual' AND status = 'active'
9088
+ `).get(scope.projectId);
9089
+ const alreadyActive = existing?.status === "active" || duplicates.some((item) => item.status === "active");
9090
+ if (!alreadyActive && Number(count?.count ?? 0) >= MAX_MANUAL_NODES) {
8904
9091
  throw new Error("Project memory limit reached; resolve an existing memory before saving another");
8905
9092
  }
8906
9093
  node.relatedPaths = Array.from(new Set([
@@ -8987,6 +9174,19 @@ function parsePaths(value) {
8987
9174
  return [];
8988
9175
  }
8989
9176
  }
9177
+ function latestLineageVersions(db, scope) {
9178
+ const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
9179
+ const latest = new Map;
9180
+ for (const row of lineageFactRows(db, scope)) {
9181
+ const key = row.kind + ":" + row.fact_key;
9182
+ const rowRank = rank.get(row.branch_head_id ?? "") ?? -1;
9183
+ const previous = latest.get(key);
9184
+ if (!previous || rowRank > previous.rank || rowRank === previous.rank && row.updated_at > previous.updatedAt) {
9185
+ latest.set(key, { id: row.id, status: row.status, rank: rowRank, updatedAt: row.updated_at });
9186
+ }
9187
+ }
9188
+ return new Map([...latest].map(([key, value]) => [key, { id: value.id, status: value.status }]));
9189
+ }
8990
9190
  function recallContext(scope, query, options = {}) {
8991
9191
  const terms = searchTerms(query.slice(0, 500));
8992
9192
  if (!terms.length)
@@ -9009,7 +9209,8 @@ function recallContext(scope, query, options = {}) {
9009
9209
  candidates.set(neighbor.row.id, { row: neighbor.row, lexical: 0, graph: neighbor.weight });
9010
9210
  }
9011
9211
  const allowedKinds = options.kinds?.length ? new Set(options.kinds) : null;
9012
- const branchIds = new Set(scope.branchEntryIds ?? []);
9212
+ const branchIds = new Set(branchLineage(scope));
9213
+ const latestVersions = latestLineageVersions(db, scope);
9013
9214
  const kindBoost = {
9014
9215
  decision: 0.1,
9015
9216
  constraint: 0.1,
@@ -9028,6 +9229,11 @@ function recallContext(scope, query, options = {}) {
9028
9229
  return [];
9029
9230
  if (allowedKinds && !allowedKinds.has(row.kind))
9030
9231
  return [];
9232
+ if (row.source === "compaction" && sameSession && sameBranch) {
9233
+ const latest = latestVersions.get(row.kind + ":" + row.fact_key);
9234
+ if (latest && (latest.status !== "active" || latest.id !== row.id))
9235
+ return [];
9236
+ }
9031
9237
  const recency = Math.max(0, 1 - (now - row.updated_at) / NINETY_DAYS_MS);
9032
9238
  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
9239
  return [{ row, score, sameSession, sameBranch }];
@@ -9750,8 +9956,8 @@ function createCompactionCommitStore(options = {}) {
9750
9956
 
9751
9957
  // src/app/native-continuity-bridge.ts
9752
9958
  import crypto6 from "crypto";
9753
- import fs8 from "fs";
9754
- import path11 from "path";
9959
+ import fs9 from "fs";
9960
+ import path12 from "path";
9755
9961
  var MAX_TEXT_BYTES = 256 * 1024;
9756
9962
  function sameScope(a, b) {
9757
9963
  return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
@@ -9761,14 +9967,14 @@ function createNativeContinuityBridge(opts = {}) {
9761
9967
  const maxEntries = Math.max(1, opts.maxEntries ?? 64);
9762
9968
  const now = opts.now ?? Date.now;
9763
9969
  const dir = opts.dir ?? nativeContinuityDir();
9764
- const lockTarget = path11.join(dir, "bridge");
9765
- const fileFor = (scope) => path11.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
9970
+ const lockTarget = path12.join(dir, "bridge");
9971
+ const fileFor = (scope) => path12.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
9766
9972
  const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
9767
9973
  const readEntry = (file) => {
9768
9974
  try {
9769
- if (fs8.statSync(file).size > MAX_TEXT_BYTES * 2)
9975
+ if (fs9.statSync(file).size > MAX_TEXT_BYTES * 2)
9770
9976
  return null;
9771
- const value = JSON.parse(fs8.readFileSync(file, "utf8"));
9977
+ const value = JSON.parse(fs9.readFileSync(file, "utf8"));
9772
9978
  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
9979
  return null;
9774
9980
  return value;
@@ -9780,16 +9986,16 @@ function createNativeContinuityBridge(opts = {}) {
9780
9986
  const fresh = [];
9781
9987
  let files = [];
9782
9988
  try {
9783
- files = fs8.readdirSync(dir).filter((file) => file.endsWith(".json"));
9989
+ files = fs9.readdirSync(dir).filter((file) => file.endsWith(".json"));
9784
9990
  } catch {
9785
9991
  return fresh;
9786
9992
  }
9787
9993
  for (const name of files) {
9788
- const file = path11.join(dir, name);
9994
+ const file = path12.join(dir, name);
9789
9995
  const entry = readEntry(file);
9790
9996
  if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
9791
9997
  try {
9792
- fs8.unlinkSync(file);
9998
+ fs9.unlinkSync(file);
9793
9999
  } catch {}
9794
10000
  } else {
9795
10001
  fresh.push({ file, entry });
@@ -9800,7 +10006,7 @@ function createNativeContinuityBridge(opts = {}) {
9800
10006
  const oldest = fresh.shift();
9801
10007
  if (oldest)
9802
10008
  try {
9803
- fs8.unlinkSync(oldest.file);
10009
+ fs9.unlinkSync(oldest.file);
9804
10010
  } catch {}
9805
10011
  }
9806
10012
  return fresh;
@@ -9808,7 +10014,7 @@ function createNativeContinuityBridge(opts = {}) {
9808
10014
  const locked = (work) => {
9809
10015
  ensureDir(dir);
9810
10016
  try {
9811
- fs8.chmodSync(dir, 448);
10017
+ fs9.chmodSync(dir, 448);
9812
10018
  } catch {}
9813
10019
  const release = acquireLockSync(lockTarget);
9814
10020
  try {
@@ -9825,13 +10031,13 @@ function createNativeContinuityBridge(opts = {}) {
9825
10031
  locked(() => {
9826
10032
  const target = fileFor(scope);
9827
10033
  try {
9828
- fs8.unlinkSync(target);
10034
+ fs9.unlinkSync(target);
9829
10035
  } catch {}
9830
10036
  prune(1);
9831
10037
  const entry = { schemaVersion: 1, scope, text, createdAt: now() };
9832
10038
  atomicWriteFileSync(target, JSON.stringify(entry));
9833
10039
  try {
9834
- fs8.chmodSync(target, 384);
10040
+ fs9.chmodSync(target, 384);
9835
10041
  } catch {}
9836
10042
  });
9837
10043
  } catch (error2) {
@@ -9849,7 +10055,7 @@ function createNativeContinuityBridge(opts = {}) {
9849
10055
  if (!entry)
9850
10056
  return null;
9851
10057
  try {
9852
- fs8.unlinkSync(target);
10058
+ fs9.unlinkSync(target);
9853
10059
  } catch {
9854
10060
  return null;
9855
10061
  }
@@ -9865,13 +10071,13 @@ function createNativeContinuityBridge(opts = {}) {
9865
10071
  locked(() => {
9866
10072
  if (scope) {
9867
10073
  try {
9868
- fs8.unlinkSync(fileFor(scope));
10074
+ fs9.unlinkSync(fileFor(scope));
9869
10075
  } catch {}
9870
10076
  return;
9871
10077
  }
9872
10078
  for (const item of prune(0)) {
9873
10079
  try {
9874
- fs8.unlinkSync(item.file);
10080
+ fs9.unlinkSync(item.file);
9875
10081
  } catch {}
9876
10082
  }
9877
10083
  });
@@ -9932,15 +10138,16 @@ function resolveModels(ctx, primary, config, explicit = false) {
9932
10138
  return { segModel, sumModel, verifyModel };
9933
10139
  }
9934
10140
  function resolveGraphScope(ctx) {
10141
+ const projectId = deriveProjectIdFromCwd(ctx.cwd);
9935
10142
  const sessionId = resolveSessionId(ctx);
9936
- if (isUnresolvedSessionId(sessionId))
10143
+ if (!projectId || isUnresolvedSessionId(sessionId))
9937
10144
  return null;
9938
- const branchEntryIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10145
+ const ancestryIds = branchEntryIds(ctx.sessionManager.getBranch());
9939
10146
  return {
9940
- projectId: deriveProjectIdFromCwd(ctx.cwd),
10147
+ projectId,
9941
10148
  sessionId,
9942
- branchHeadId: branchEntryIds.at(-1),
9943
- branchEntryIds
10149
+ branchHeadId: ancestryIds.at(-1),
10150
+ branchEntryIds: ancestryIds
9944
10151
  };
9945
10152
  }
9946
10153
  function smartCompactExtension(pi) {
@@ -10022,7 +10229,7 @@ function smartCompactExtension(pi) {
10022
10229
  }
10023
10230
  const scope = resolveGraphScope(ctx);
10024
10231
  if (!scope)
10025
- return { content: [{ type: "text", text: "Smart Recall needs a persisted session id." }], details: undefined };
10232
+ return { content: [{ type: "text", text: "Smart Recall must run from a project directory and needs a persisted session id." }], details: undefined };
10026
10233
  const results = recallContext(scope, params.query, {
10027
10234
  limit: params.limit,
10028
10235
  sessionOnly: params.scope === "session",
@@ -10056,11 +10263,11 @@ function smartCompactExtension(pi) {
10056
10263
  }
10057
10264
  const scope = resolveGraphScope(ctx);
10058
10265
  if (!scope)
10059
- return { content: [{ type: "text", text: "Saving memory needs a persisted session id." }], details: undefined };
10266
+ return { content: [{ type: "text", text: "Saving project memory must run from a project directory and needs a persisted session id." }], details: undefined };
10060
10267
  const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
10061
10268
  const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
10062
10269
  const content = scrubber.scrubText(params.content).value;
10063
- const relatedPaths = (params.related_paths ?? []).map((path12) => scrubber.scrubText(path12).value);
10270
+ const relatedPaths = (params.related_paths ?? []).map((path13) => scrubber.scrubText(path13).value);
10064
10271
  const status = params.status ?? "active";
10065
10272
  if (!ctx.hasUI) {
10066
10273
  return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
@@ -10068,7 +10275,7 @@ function smartCompactExtension(pi) {
10068
10275
  const approved = await ctx.ui.confirm(status === "resolved" ? "Resolve Project Memory" : "Save Project Memory", "Kind: " + params.kind + `
10069
10276
  Title: ` + title + `
10070
10277
 
10071
- ` + content.slice(0, 800) + (relatedPaths.length ? `
10278
+ ` + content + (relatedPaths.length ? `
10072
10279
 
10073
10280
  Paths: ` + relatedPaths.join(", ") : ""));
10074
10281
  if (!approved || signal?.aborted) {
@@ -10109,16 +10316,16 @@ Paths: ` + relatedPaths.join(", ") : ""));
10109
10316
  const maxLlmCalls = maxCallsRaw == null ? undefined : Number(maxCallsRaw);
10110
10317
  const maxLlmInputTokens = maxInputRaw == null ? undefined : Number(maxInputRaw);
10111
10318
  const maxLatencyMs = maxLatencyRaw == null ? undefined : Number(maxLatencyRaw);
10112
- if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls < 1 || maxLlmCalls > 100)) {
10113
- ctx.ui.notify("--max-calls must be an integer from 1 to 100", "error");
10319
+ if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls < BUDGET_LIMITS.CALLS.min || maxLlmCalls > BUDGET_LIMITS.CALLS.max)) {
10320
+ ctx.ui.notify("--max-calls must be an integer from " + BUDGET_LIMITS.CALLS.min + " to " + BUDGET_LIMITS.CALLS.max, "error");
10114
10321
  return;
10115
10322
  }
10116
- if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens < 1e4 || maxLlmInputTokens > 1e6)) {
10117
- ctx.ui.notify("--max-input-tokens must be an integer from 10000 to 1000000", "error");
10323
+ if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens < BUDGET_LIMITS.INPUT_TOKENS.min || maxLlmInputTokens > BUDGET_LIMITS.INPUT_TOKENS.max)) {
10324
+ ctx.ui.notify("--max-input-tokens must be an integer from " + BUDGET_LIMITS.INPUT_TOKENS.min + " to " + BUDGET_LIMITS.INPUT_TOKENS.max, "error");
10118
10325
  return;
10119
10326
  }
10120
- if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs < 5000 || maxLatencyMs > 600000)) {
10121
- ctx.ui.notify("--max-latency must be 5000\u2013600000 ms", "error");
10327
+ if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs < BUDGET_LIMITS.LATENCY_MS.min || maxLatencyMs > BUDGET_LIMITS.LATENCY_MS.max)) {
10328
+ ctx.ui.notify("--max-latency must be " + BUDGET_LIMITS.LATENCY_MS.min + "\u2013" + BUDGET_LIMITS.LATENCY_MS.max + " ms", "error");
10122
10329
  return;
10123
10330
  }
10124
10331
  if (flags.includes("metrics") || flags.includes("dashboard")) {
@@ -10186,8 +10393,12 @@ Paths: ` + relatedPaths.join(", ") : ""));
10186
10393
  }
10187
10394
  if (flags.includes("loops")) {
10188
10395
  const projectId = deriveProjectIdFromCwd(ctx.cwd);
10396
+ if (!projectId) {
10397
+ ctx.ui.notify("Project loops must be managed from a project directory", "warning");
10398
+ return;
10399
+ }
10189
10400
  const sessionId = resolveSessionId(ctx);
10190
- const branchIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10401
+ const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
10191
10402
  const state = isUnresolvedSessionId(sessionId) ? null : loadScopedCompactionState({ projectId, sessionId }, branchIds);
10192
10403
  if (!state || state.openLoops.length === 0) {
10193
10404
  ctx.ui.notify("No persisted open loops for this project", "info");
@@ -10368,7 +10579,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10368
10579
  if (isUnresolvedSessionId(sessionId))
10369
10580
  return;
10370
10581
  const projectId = deriveProjectIdFromCwd(ctx.cwd);
10371
- const branchIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10582
+ if (!projectId)
10583
+ return;
10584
+ const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
10372
10585
  const branchHeadId = typeof event.compactionEntry.id === "string" ? event.compactionEntry.id : branchIds.at(-1);
10373
10586
  if (!branchHeadId)
10374
10587
  return;
@@ -10444,9 +10657,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10444
10657
  report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
10445
10658
  dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." },
10446
10659
  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 (1-100)." },
10448
- max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (10000-1000000)." },
10449
- max_latency_ms: { type: "number", description: "Optional hard pipeline latency budget in milliseconds (5000-600000). Modes use soft latency targets by default." }
10660
+ max_calls: { type: "number", description: "Maximum LLM calls for this run (" + BUDGET_LIMITS.CALLS.min + "-" + BUDGET_LIMITS.CALLS.max + ")." },
10661
+ max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (" + BUDGET_LIMITS.INPUT_TOKENS.min + "-" + BUDGET_LIMITS.INPUT_TOKENS.max + ")." },
10662
+ max_latency_ms: { type: "number", description: "Optional pipeline cancellation budget in milliseconds (" + BUDGET_LIMITS.LATENCY_MS.min + "-" + BUDGET_LIMITS.LATENCY_MS.max + ")." }
10450
10663
  }
10451
10664
  },
10452
10665
  async execute(_id, params, signal, _onUp, ctx) {
@@ -10455,9 +10668,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10455
10668
  const verbose = !!params.verbose;
10456
10669
  const dryRun = !!params.dry_run;
10457
10670
  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 >= 1 && params.max_calls <= 100 ? params.max_calls : undefined;
10459
- const maxLlmInputTokens = typeof params.max_input_tokens === "number" && Number.isInteger(params.max_input_tokens) && params.max_input_tokens >= 1e4 && params.max_input_tokens <= 1e6 ? params.max_input_tokens : undefined;
10460
- const maxLatencyMs = typeof params.max_latency_ms === "number" && params.max_latency_ms >= 5000 && params.max_latency_ms <= 600000 ? params.max_latency_ms : undefined;
10671
+ 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;
10672
+ 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;
10673
+ 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
10674
  if (params.report || params.dashboard) {
10462
10675
  const report = buildMetricsReport();
10463
10676
  const fp = params.dashboard ? writeMetricsDashboard() : null;