pi-smart-compact 8.0.6 → 8.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +513 -0
  2. package/CHANGELOG.md +19 -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 +759 -548
  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 +150 -110
  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/fingerprint.d.ts +1 -1
  36. package/dist/utils/fingerprint.d.ts.map +1 -1
  37. package/dist/utils/pruning.d.ts.map +1 -1
  38. package/dist/utils/state.d.ts +4 -3
  39. package/dist/utils/state.d.ts.map +1 -1
  40. package/dist/utils/tokens.d.ts.map +1 -1
  41. package/docs/MIGRATING_TO_V8.md +30 -13
  42. package/docs/RELEASE.md +13 -10
  43. package/package.json +2 -1
  44. package/dist/app/explore-wrap.d.ts +0 -8
  45. 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.7";
109
106
  var CHARS_PER_TOKEN = 3.8;
110
107
  var MIN_COMPACTION_SAVING_RATIO = 0.1;
111
108
  var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
109
+ var POST_SUMMARY_RESERVE_RATIO = 0.25;
110
+ var BUDGET_LIMITS = {
111
+ CALLS: { min: 1, max: 100 },
112
+ INPUT_TOKENS: { min: 1e4, max: 1e6 },
113
+ LATENCY_MS: { min: 5000, max: 600000 }
114
+ };
112
115
  var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
113
116
  var PROFILES = {
114
117
  light: {
@@ -480,13 +483,8 @@ var LOCK_STALE_MS = 5000;
480
483
  var LOCK_RETRY_MS = 25;
481
484
  var LOCK_MAX_RETRIES = 80;
482
485
  function ensureDir(dir) {
483
- 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
  }
@@ -8344,6 +8472,10 @@ async function verifyAndPatch(rc) {
8344
8472
  return advance(out, "_verified");
8345
8473
  }
8346
8474
 
8475
+ // src/app/steps/state.ts
8476
+ import fs7 from "fs";
8477
+ import path10 from "path";
8478
+
8347
8479
  // src/domain/yield-gate.ts
8348
8480
  class YieldGateError extends Error {
8349
8481
  reason;
@@ -8424,9 +8556,14 @@ function buildState(rc) {
8424
8556
  const nextActions = extractNextActions(summary);
8425
8557
  const criticalContextItems = extractCriticalContext(summary);
8426
8558
  const currentState = buildCompactionState(extraction, managedLoops, rc.explorationReport, nextActions, criticalContextItems, loopOverrides);
8559
+ const summarizedGoal = summaryEvidenceLine(findSection(summary, "goal")?.body ?? "", TRUNC.MESSAGE);
8427
8560
  currentState.scope = rc.continuityScope;
8428
8561
  currentState.factOverrides = prevState?.factOverrides ?? [];
8429
8562
  let compactionState = mergeCompactionStates(prevState, currentState);
8563
+ compactionState.deletedFiles = compactionState.deletedFiles.filter((file) => {
8564
+ const candidate = path10.isAbsolute(file) ? file : path10.resolve(rc.ctx.cwd, file);
8565
+ return !fs7.existsSync(candidate);
8566
+ });
8430
8567
  if (preserve.length > 0) {
8431
8568
  compactionState.readFiles = Array.from(new Set([...preserve, ...compactionState.readFiles])).slice(0, 100);
8432
8569
  }
@@ -8439,6 +8576,8 @@ function buildState(rc) {
8439
8576
  rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
8440
8577
  }
8441
8578
  }
8579
+ if (summarizedGoal && currentState.goal)
8580
+ compactionState.goal = summarizedGoal;
8442
8581
  const continuity = renderContinuityCapsule(compactionState, undefined, summary);
8443
8582
  if (continuity)
8444
8583
  summary = summary.trimEnd() + `
@@ -8512,8 +8651,8 @@ function buildState(rc) {
8512
8651
 
8513
8652
  // src/infra/context-graph.ts
8514
8653
  import { createHash as createHash3 } from "crypto";
8515
- import fs7 from "fs";
8516
- import path10 from "path";
8654
+ import fs8 from "fs";
8655
+ import path11 from "path";
8517
8656
  import { createRequire } from "module";
8518
8657
  var require2 = createRequire(import.meta.url);
8519
8658
  var MAX_PROJECT_NODES = 2000;
@@ -8521,6 +8660,7 @@ var MAX_MANUAL_NODES = 500;
8521
8660
  var MAX_SESSION_NODES = 256;
8522
8661
  var MAX_QUERY_CANDIDATES = 80;
8523
8662
  var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
8663
+ var CONTEXT_GRAPH_SCHEMA_VERSION = 1;
8524
8664
  function nodeSqliteAdapter(db) {
8525
8665
  return {
8526
8666
  exec: (sql) => db.exec(sql),
@@ -8543,7 +8683,7 @@ function nodeSqliteAdapter(db) {
8543
8683
  }
8544
8684
  function openDatabase() {
8545
8685
  const fp = contextGraphFile();
8546
- fs7.mkdirSync(path10.dirname(fp), { recursive: true });
8686
+ fs8.mkdirSync(path11.dirname(fp), { recursive: true });
8547
8687
  let db;
8548
8688
  if ("bun" in process.versions) {
8549
8689
  const { Database } = require2("bun:sqlite");
@@ -8553,7 +8693,7 @@ function openDatabase() {
8553
8693
  db = nodeSqliteAdapter(new DatabaseSync(fp));
8554
8694
  }
8555
8695
  try {
8556
- fs7.chmodSync(fp, 384);
8696
+ fs8.chmodSync(fp, 384);
8557
8697
  } catch {}
8558
8698
  db.exec("PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;");
8559
8699
  db.exec(`
@@ -8573,8 +8713,6 @@ function openDatabase() {
8573
8713
  created_at INTEGER NOT NULL,
8574
8714
  updated_at INTEGER NOT NULL
8575
8715
  );
8576
- CREATE UNIQUE INDEX IF NOT EXISTS context_nodes_fact
8577
- ON context_nodes(project_id, session_id, kind, fact_key);
8578
8716
  CREATE INDEX IF NOT EXISTS context_nodes_project_status
8579
8717
  ON context_nodes(project_id, status, updated_at DESC);
8580
8718
  CREATE TABLE IF NOT EXISTS context_edges (
@@ -8592,6 +8730,20 @@ function openDatabase() {
8592
8730
  tokenize='unicode61 remove_diacritics 2'
8593
8731
  );
8594
8732
  `);
8733
+ const version = db.query("PRAGMA user_version").get();
8734
+ if (Number(version?.user_version ?? 0) < CONTEXT_GRAPH_SCHEMA_VERSION) {
8735
+ db.transaction(() => {
8736
+ db.exec(`
8737
+ DROP INDEX IF EXISTS context_nodes_fact;
8738
+ DELETE FROM context_nodes_fts
8739
+ WHERE node_id IN (SELECT id FROM context_nodes WHERE source = 'compaction');
8740
+ DELETE FROM context_nodes WHERE source = 'compaction';
8741
+ CREATE UNIQUE INDEX context_nodes_fact
8742
+ ON context_nodes(project_id, session_id, kind, fact_key, COALESCE(branch_head_id, ''));
8743
+ PRAGMA user_version = ${CONTEXT_GRAPH_SCHEMA_VERSION};
8744
+ `);
8745
+ })();
8746
+ }
8595
8747
  return db;
8596
8748
  }
8597
8749
  function stableId(...parts) {
@@ -8641,7 +8793,7 @@ function makeNode(scope, kind, title, content, options = {}) {
8641
8793
  const key = factKey(content);
8642
8794
  const now = Date.now();
8643
8795
  return {
8644
- id: stableId(scope.projectId, scope.sessionId, kind, key),
8796
+ id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
8645
8797
  projectId: scope.projectId,
8646
8798
  sessionId: scope.sessionId,
8647
8799
  branchHeadId: scope.branchHeadId ?? null,
@@ -8668,27 +8820,55 @@ function ensureFileNode(db, scope, file, now, content = file) {
8668
8820
  upsertNode(db, node);
8669
8821
  return node;
8670
8822
  }
8671
- function markFactStatus(db, scope, kind, key, status) {
8823
+ function branchLineage(scope) {
8824
+ return Array.from(new Set([...scope.branchEntryIds ?? [], scope.branchHeadId].filter((id) => typeof id === "string" && id.length > 0)));
8825
+ }
8826
+ function lineageFactRows(db, scope, kind, key) {
8827
+ const lineage = new Set(branchLineage(scope));
8672
8828
  const rows = db.query(`
8673
- SELECT 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)
8829
+ SELECT * FROM context_nodes
8830
+ WHERE project_id = ? AND session_id = ? AND source = 'compaction'
8831
+ ${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
8832
+ `).all(scope.projectId, scope.sessionId, ...kind ? [kind] : [], ...key ? [key] : []);
8833
+ return rows.filter((row) => lineage.size > 0 ? Boolean(row.branch_head_id && lineage.has(row.branch_head_id)) : row.branch_head_id == null);
8834
+ }
8835
+ function latestLineageFact(db, scope, kind, key) {
8836
+ const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
8837
+ return lineageFactRows(db, scope, kind, key).sort((a, b) => (rank.get(b.branch_head_id ?? "") ?? -1) - (rank.get(a.branch_head_id ?? "") ?? -1) || b.updated_at - a.updated_at)[0] ?? null;
8838
+ }
8839
+ function markFactStatus(db, scope, kind, key, status) {
8840
+ const previous = latestLineageFact(db, scope, kind, key);
8841
+ if (!previous || previous.status === status)
8677
8842
  return;
8678
- 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);
8843
+ const now = Date.now();
8844
+ upsertNode(db, {
8845
+ id: stableId(scope.projectId, scope.sessionId, kind, key, scope.branchHeadId ?? ""),
8846
+ projectId: scope.projectId,
8847
+ sessionId: scope.sessionId,
8848
+ branchHeadId: scope.branchHeadId ?? null,
8849
+ kind,
8850
+ factKey: key,
8851
+ title: previous.title,
8852
+ content: previous.content,
8853
+ status,
8854
+ source: "compaction",
8855
+ confidence: previous.confidence,
8856
+ relatedPaths: parsePaths(previous.related_paths),
8857
+ createdAt: now,
8858
+ updatedAt: now
8859
+ }, true);
8860
+ }
8861
+ function sameActiveFact(row, node) {
8862
+ return Boolean(row && row.status === "active" && node.status === "active" && row.title === node.title && row.content === node.content && row.confidence === node.confidence && JSON.stringify(parsePaths(row.related_paths)) === JSON.stringify(node.relatedPaths));
8685
8863
  }
8686
8864
  function addFact(db, scope, sessionNodeId, kind, title, content, relatedPaths = [], confidence = 0.85, keyText = content) {
8687
8865
  if (!content.trim())
8688
8866
  return;
8689
8867
  const node = makeNode(scope, kind, title, content, { relatedPaths, confidence });
8690
8868
  node.factKey = factKey(keyText);
8691
- node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey);
8869
+ node.id = stableId(scope.projectId, scope.sessionId, kind, node.factKey, scope.branchHeadId ?? "");
8870
+ if (sameActiveFact(latestLineageFact(db, scope, kind, node.factKey), node))
8871
+ return;
8692
8872
  upsertNode(db, node);
8693
8873
  linkNodes(db, scope.projectId, sessionNodeId, node.id, "contains", 1, node.updatedAt);
8694
8874
  for (const file of relatedPaths) {
@@ -8729,7 +8909,8 @@ function indexCompactionState(projectId, state) {
8729
8909
  const scope = {
8730
8910
  projectId,
8731
8911
  sessionId,
8732
- branchHeadId: state.scope.branchHeadId
8912
+ branchHeadId: state.scope.branchHeadId,
8913
+ branchEntryIds: state.scope.branchAncestryIds
8733
8914
  };
8734
8915
  let db = null;
8735
8916
  try {
@@ -8745,17 +8926,15 @@ function indexCompactionState(projectId, state) {
8745
8926
  upsertNode(db, projectNode);
8746
8927
  upsertNode(db, sessionNode);
8747
8928
  linkNodes(db, projectId, projectNode.id, sessionNode.id, "contains", 1, now);
8748
- 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)
8929
+ if (state.goal) {
8930
+ const currentGoalKey = factKey(state.goal);
8931
+ const priorGoalKeys = new Set(lineageFactRows(db, scope, "goal").map((row) => row.fact_key));
8932
+ for (const key of priorGoalKeys) {
8933
+ if (key !== currentGoalKey)
8934
+ markFactStatus(db, scope, "goal", key, "superseded");
8935
+ }
8758
8936
  addFact(db, scope, sessionNode.id, "goal", "Current goal", state.goal, [], 0.98);
8937
+ }
8759
8938
  for (const item of state.decisions) {
8760
8939
  addFact(db, scope, sessionNode.id, "decision", "Decision", item.summary + (item.userResponse ? " \u2192 " + item.userResponse : ""), [], item.type === "explicit" ? 0.98 : 0.82, item.summary);
8761
8940
  }
@@ -8774,6 +8953,8 @@ function indexCompactionState(projectId, state) {
8774
8953
  relatedPaths: item.files,
8775
8954
  confidence: item.priority === "critical" || item.priority === "high" ? 0.98 : 0.88
8776
8955
  });
8956
+ if (sameActiveFact(latestLineageFact(db, scope, "loop", node.factKey), node))
8957
+ continue;
8777
8958
  upsertNode(db, node);
8778
8959
  linkNodes(db, projectId, sessionNode.id, node.id, "contains", 1, now);
8779
8960
  for (const file of item.files) {
@@ -8894,13 +9075,17 @@ function saveContextMemory(scope, memory) {
8894
9075
  node.sessionId = "*";
8895
9076
  node.branchHeadId = null;
8896
9077
  const transaction = db.transaction(() => {
8897
- const exists = db.query("SELECT 1 AS found FROM context_nodes WHERE id = ?").get(node.id);
9078
+ const existing = db.query("SELECT status FROM context_nodes WHERE id = ?").get(node.id);
8898
9079
  const duplicates = db.query(`
8899
- SELECT id, related_paths FROM context_nodes
9080
+ SELECT id, related_paths, status FROM context_nodes
8900
9081
  WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND id <> ?
8901
9082
  `).all(scope.projectId, memory.kind, node.factKey, node.id);
8902
- const count = db.query("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) {
9083
+ const count = db.query(`
9084
+ SELECT count(*) AS count FROM context_nodes
9085
+ WHERE project_id = ? AND source = 'manual' AND status = 'active'
9086
+ `).get(scope.projectId);
9087
+ const alreadyActive = existing?.status === "active" || duplicates.some((item) => item.status === "active");
9088
+ if (!alreadyActive && Number(count?.count ?? 0) >= MAX_MANUAL_NODES) {
8904
9089
  throw new Error("Project memory limit reached; resolve an existing memory before saving another");
8905
9090
  }
8906
9091
  node.relatedPaths = Array.from(new Set([
@@ -8987,6 +9172,19 @@ function parsePaths(value) {
8987
9172
  return [];
8988
9173
  }
8989
9174
  }
9175
+ function latestLineageVersions(db, scope) {
9176
+ const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
9177
+ const latest = new Map;
9178
+ for (const row of lineageFactRows(db, scope)) {
9179
+ const key = row.kind + ":" + row.fact_key;
9180
+ const rowRank = rank.get(row.branch_head_id ?? "") ?? -1;
9181
+ const previous = latest.get(key);
9182
+ if (!previous || rowRank > previous.rank || rowRank === previous.rank && row.updated_at > previous.updatedAt) {
9183
+ latest.set(key, { id: row.id, status: row.status, rank: rowRank, updatedAt: row.updated_at });
9184
+ }
9185
+ }
9186
+ return new Map([...latest].map(([key, value]) => [key, { id: value.id, status: value.status }]));
9187
+ }
8990
9188
  function recallContext(scope, query, options = {}) {
8991
9189
  const terms = searchTerms(query.slice(0, 500));
8992
9190
  if (!terms.length)
@@ -9009,7 +9207,8 @@ function recallContext(scope, query, options = {}) {
9009
9207
  candidates.set(neighbor.row.id, { row: neighbor.row, lexical: 0, graph: neighbor.weight });
9010
9208
  }
9011
9209
  const allowedKinds = options.kinds?.length ? new Set(options.kinds) : null;
9012
- const branchIds = new Set(scope.branchEntryIds ?? []);
9210
+ const branchIds = new Set(branchLineage(scope));
9211
+ const latestVersions = latestLineageVersions(db, scope);
9013
9212
  const kindBoost = {
9014
9213
  decision: 0.1,
9015
9214
  constraint: 0.1,
@@ -9028,6 +9227,11 @@ function recallContext(scope, query, options = {}) {
9028
9227
  return [];
9029
9228
  if (allowedKinds && !allowedKinds.has(row.kind))
9030
9229
  return [];
9230
+ if (row.source === "compaction" && sameSession && sameBranch) {
9231
+ const latest = latestVersions.get(row.kind + ":" + row.fact_key);
9232
+ if (latest && (latest.status !== "active" || latest.id !== row.id))
9233
+ return [];
9234
+ }
9031
9235
  const recency = Math.max(0, 1 - (now - row.updated_at) / NINETY_DAYS_MS);
9032
9236
  const score = Math.min(1, 0.05 + lexical * 0.3 + graph * 0.15 + (sameBranch ? 0.4 : sameSession ? 0.05 : 0) + (kindBoost[row.kind] ?? 0.03) + Math.max(0, Math.min(1, row.confidence)) * 0.08 + recency * 0.05 + (row.source === "manual" ? 0.04 : 0));
9033
9237
  return [{ row, score, sameSession, sameBranch }];
@@ -9750,8 +9954,8 @@ function createCompactionCommitStore(options = {}) {
9750
9954
 
9751
9955
  // src/app/native-continuity-bridge.ts
9752
9956
  import crypto6 from "crypto";
9753
- import fs8 from "fs";
9754
- import path11 from "path";
9957
+ import fs9 from "fs";
9958
+ import path12 from "path";
9755
9959
  var MAX_TEXT_BYTES = 256 * 1024;
9756
9960
  function sameScope(a, b) {
9757
9961
  return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
@@ -9761,14 +9965,14 @@ function createNativeContinuityBridge(opts = {}) {
9761
9965
  const maxEntries = Math.max(1, opts.maxEntries ?? 64);
9762
9966
  const now = opts.now ?? Date.now;
9763
9967
  const dir = opts.dir ?? nativeContinuityDir();
9764
- const lockTarget = 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");
9968
+ const lockTarget = path12.join(dir, "bridge");
9969
+ const fileFor = (scope) => path12.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
9766
9970
  const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
9767
9971
  const readEntry = (file) => {
9768
9972
  try {
9769
- if (fs8.statSync(file).size > MAX_TEXT_BYTES * 2)
9973
+ if (fs9.statSync(file).size > MAX_TEXT_BYTES * 2)
9770
9974
  return null;
9771
- const value = JSON.parse(fs8.readFileSync(file, "utf8"));
9975
+ const value = JSON.parse(fs9.readFileSync(file, "utf8"));
9772
9976
  if (value.schemaVersion !== 1 || typeof value.text !== "string" || Buffer.byteLength(value.text) > MAX_TEXT_BYTES || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt) || !value.scope || !validScope(value.scope))
9773
9977
  return null;
9774
9978
  return value;
@@ -9780,16 +9984,16 @@ function createNativeContinuityBridge(opts = {}) {
9780
9984
  const fresh = [];
9781
9985
  let files = [];
9782
9986
  try {
9783
- files = fs8.readdirSync(dir).filter((file) => file.endsWith(".json"));
9987
+ files = fs9.readdirSync(dir).filter((file) => file.endsWith(".json"));
9784
9988
  } catch {
9785
9989
  return fresh;
9786
9990
  }
9787
9991
  for (const name of files) {
9788
- const file = path11.join(dir, name);
9992
+ const file = path12.join(dir, name);
9789
9993
  const entry = readEntry(file);
9790
9994
  if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
9791
9995
  try {
9792
- fs8.unlinkSync(file);
9996
+ fs9.unlinkSync(file);
9793
9997
  } catch {}
9794
9998
  } else {
9795
9999
  fresh.push({ file, entry });
@@ -9800,7 +10004,7 @@ function createNativeContinuityBridge(opts = {}) {
9800
10004
  const oldest = fresh.shift();
9801
10005
  if (oldest)
9802
10006
  try {
9803
- fs8.unlinkSync(oldest.file);
10007
+ fs9.unlinkSync(oldest.file);
9804
10008
  } catch {}
9805
10009
  }
9806
10010
  return fresh;
@@ -9808,7 +10012,7 @@ function createNativeContinuityBridge(opts = {}) {
9808
10012
  const locked = (work) => {
9809
10013
  ensureDir(dir);
9810
10014
  try {
9811
- fs8.chmodSync(dir, 448);
10015
+ fs9.chmodSync(dir, 448);
9812
10016
  } catch {}
9813
10017
  const release = acquireLockSync(lockTarget);
9814
10018
  try {
@@ -9825,13 +10029,13 @@ function createNativeContinuityBridge(opts = {}) {
9825
10029
  locked(() => {
9826
10030
  const target = fileFor(scope);
9827
10031
  try {
9828
- fs8.unlinkSync(target);
10032
+ fs9.unlinkSync(target);
9829
10033
  } catch {}
9830
10034
  prune(1);
9831
10035
  const entry = { schemaVersion: 1, scope, text, createdAt: now() };
9832
10036
  atomicWriteFileSync(target, JSON.stringify(entry));
9833
10037
  try {
9834
- fs8.chmodSync(target, 384);
10038
+ fs9.chmodSync(target, 384);
9835
10039
  } catch {}
9836
10040
  });
9837
10041
  } catch (error2) {
@@ -9849,7 +10053,7 @@ function createNativeContinuityBridge(opts = {}) {
9849
10053
  if (!entry)
9850
10054
  return null;
9851
10055
  try {
9852
- fs8.unlinkSync(target);
10056
+ fs9.unlinkSync(target);
9853
10057
  } catch {
9854
10058
  return null;
9855
10059
  }
@@ -9865,13 +10069,13 @@ function createNativeContinuityBridge(opts = {}) {
9865
10069
  locked(() => {
9866
10070
  if (scope) {
9867
10071
  try {
9868
- fs8.unlinkSync(fileFor(scope));
10072
+ fs9.unlinkSync(fileFor(scope));
9869
10073
  } catch {}
9870
10074
  return;
9871
10075
  }
9872
10076
  for (const item of prune(0)) {
9873
10077
  try {
9874
- fs8.unlinkSync(item.file);
10078
+ fs9.unlinkSync(item.file);
9875
10079
  } catch {}
9876
10080
  }
9877
10081
  });
@@ -9932,15 +10136,16 @@ function resolveModels(ctx, primary, config, explicit = false) {
9932
10136
  return { segModel, sumModel, verifyModel };
9933
10137
  }
9934
10138
  function resolveGraphScope(ctx) {
10139
+ const projectId = deriveProjectIdFromCwd(ctx.cwd);
9935
10140
  const sessionId = resolveSessionId(ctx);
9936
- if (isUnresolvedSessionId(sessionId))
10141
+ if (!projectId || isUnresolvedSessionId(sessionId))
9937
10142
  return null;
9938
- const branchEntryIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10143
+ const ancestryIds = branchEntryIds(ctx.sessionManager.getBranch());
9939
10144
  return {
9940
- projectId: deriveProjectIdFromCwd(ctx.cwd),
10145
+ projectId,
9941
10146
  sessionId,
9942
- branchHeadId: branchEntryIds.at(-1),
9943
- branchEntryIds
10147
+ branchHeadId: ancestryIds.at(-1),
10148
+ branchEntryIds: ancestryIds
9944
10149
  };
9945
10150
  }
9946
10151
  function smartCompactExtension(pi) {
@@ -10022,7 +10227,7 @@ function smartCompactExtension(pi) {
10022
10227
  }
10023
10228
  const scope = resolveGraphScope(ctx);
10024
10229
  if (!scope)
10025
- return { content: [{ type: "text", text: "Smart Recall needs a persisted session id." }], details: undefined };
10230
+ return { content: [{ type: "text", text: "Smart Recall must run from a project directory and needs a persisted session id." }], details: undefined };
10026
10231
  const results = recallContext(scope, params.query, {
10027
10232
  limit: params.limit,
10028
10233
  sessionOnly: params.scope === "session",
@@ -10056,11 +10261,11 @@ function smartCompactExtension(pi) {
10056
10261
  }
10057
10262
  const scope = resolveGraphScope(ctx);
10058
10263
  if (!scope)
10059
- return { content: [{ type: "text", text: "Saving memory needs a persisted session id." }], details: undefined };
10264
+ return { content: [{ type: "text", text: "Saving project memory must run from a project directory and needs a persisted session id." }], details: undefined };
10060
10265
  const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
10061
10266
  const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
10062
10267
  const content = scrubber.scrubText(params.content).value;
10063
- const relatedPaths = (params.related_paths ?? []).map((path12) => scrubber.scrubText(path12).value);
10268
+ const relatedPaths = (params.related_paths ?? []).map((path13) => scrubber.scrubText(path13).value);
10064
10269
  const status = params.status ?? "active";
10065
10270
  if (!ctx.hasUI) {
10066
10271
  return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
@@ -10068,7 +10273,7 @@ function smartCompactExtension(pi) {
10068
10273
  const approved = await ctx.ui.confirm(status === "resolved" ? "Resolve Project Memory" : "Save Project Memory", "Kind: " + params.kind + `
10069
10274
  Title: ` + title + `
10070
10275
 
10071
- ` + content.slice(0, 800) + (relatedPaths.length ? `
10276
+ ` + content + (relatedPaths.length ? `
10072
10277
 
10073
10278
  Paths: ` + relatedPaths.join(", ") : ""));
10074
10279
  if (!approved || signal?.aborted) {
@@ -10109,16 +10314,16 @@ Paths: ` + relatedPaths.join(", ") : ""));
10109
10314
  const maxLlmCalls = maxCallsRaw == null ? undefined : Number(maxCallsRaw);
10110
10315
  const maxLlmInputTokens = maxInputRaw == null ? undefined : Number(maxInputRaw);
10111
10316
  const maxLatencyMs = maxLatencyRaw == null ? undefined : Number(maxLatencyRaw);
10112
- if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls < 1 || maxLlmCalls > 100)) {
10113
- ctx.ui.notify("--max-calls must be an integer from 1 to 100", "error");
10317
+ if (maxLlmCalls !== undefined && (!Number.isInteger(maxLlmCalls) || maxLlmCalls < BUDGET_LIMITS.CALLS.min || maxLlmCalls > BUDGET_LIMITS.CALLS.max)) {
10318
+ ctx.ui.notify("--max-calls must be an integer from " + BUDGET_LIMITS.CALLS.min + " to " + BUDGET_LIMITS.CALLS.max, "error");
10114
10319
  return;
10115
10320
  }
10116
- if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens < 1e4 || maxLlmInputTokens > 1e6)) {
10117
- ctx.ui.notify("--max-input-tokens must be an integer from 10000 to 1000000", "error");
10321
+ if (maxLlmInputTokens !== undefined && (!Number.isInteger(maxLlmInputTokens) || maxLlmInputTokens < BUDGET_LIMITS.INPUT_TOKENS.min || maxLlmInputTokens > BUDGET_LIMITS.INPUT_TOKENS.max)) {
10322
+ ctx.ui.notify("--max-input-tokens must be an integer from " + BUDGET_LIMITS.INPUT_TOKENS.min + " to " + BUDGET_LIMITS.INPUT_TOKENS.max, "error");
10118
10323
  return;
10119
10324
  }
10120
- if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs < 5000 || maxLatencyMs > 600000)) {
10121
- ctx.ui.notify("--max-latency must be 5000\u2013600000 ms", "error");
10325
+ if (maxLatencyMs !== undefined && (!Number.isFinite(maxLatencyMs) || maxLatencyMs < BUDGET_LIMITS.LATENCY_MS.min || maxLatencyMs > BUDGET_LIMITS.LATENCY_MS.max)) {
10326
+ ctx.ui.notify("--max-latency must be " + BUDGET_LIMITS.LATENCY_MS.min + "\u2013" + BUDGET_LIMITS.LATENCY_MS.max + " ms", "error");
10122
10327
  return;
10123
10328
  }
10124
10329
  if (flags.includes("metrics") || flags.includes("dashboard")) {
@@ -10186,8 +10391,12 @@ Paths: ` + relatedPaths.join(", ") : ""));
10186
10391
  }
10187
10392
  if (flags.includes("loops")) {
10188
10393
  const projectId = deriveProjectIdFromCwd(ctx.cwd);
10394
+ if (!projectId) {
10395
+ ctx.ui.notify("Project loops must be managed from a project directory", "warning");
10396
+ return;
10397
+ }
10189
10398
  const sessionId = resolveSessionId(ctx);
10190
- const branchIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10399
+ const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
10191
10400
  const state = isUnresolvedSessionId(sessionId) ? null : loadScopedCompactionState({ projectId, sessionId }, branchIds);
10192
10401
  if (!state || state.openLoops.length === 0) {
10193
10402
  ctx.ui.notify("No persisted open loops for this project", "info");
@@ -10368,7 +10577,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10368
10577
  if (isUnresolvedSessionId(sessionId))
10369
10578
  return;
10370
10579
  const projectId = deriveProjectIdFromCwd(ctx.cwd);
10371
- const branchIds = ctx.sessionManager.getBranch().map((entry) => entry.id).filter((id) => typeof id === "string");
10580
+ if (!projectId)
10581
+ return;
10582
+ const branchIds = branchEntryIds(ctx.sessionManager.getBranch());
10372
10583
  const branchHeadId = typeof event.compactionEntry.id === "string" ? event.compactionEntry.id : branchIds.at(-1);
10373
10584
  if (!branchHeadId)
10374
10585
  return;
@@ -10444,9 +10655,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10444
10655
  report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
10445
10656
  dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." },
10446
10657
  focus: { type: "string", description: "Topic or path that should receive extra preservation budget." },
10447
- max_calls: { type: "number", description: "Maximum LLM calls for this run (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." }
10658
+ max_calls: { type: "number", description: "Maximum LLM calls for this run (" + BUDGET_LIMITS.CALLS.min + "-" + BUDGET_LIMITS.CALLS.max + ")." },
10659
+ max_input_tokens: { type: "number", description: "Aggregate prompt-token budget for this run (" + BUDGET_LIMITS.INPUT_TOKENS.min + "-" + BUDGET_LIMITS.INPUT_TOKENS.max + ")." },
10660
+ max_latency_ms: { type: "number", description: "Optional pipeline cancellation budget in milliseconds (" + BUDGET_LIMITS.LATENCY_MS.min + "-" + BUDGET_LIMITS.LATENCY_MS.max + ")." }
10450
10661
  }
10451
10662
  },
10452
10663
  async execute(_id, params, signal, _onUp, ctx) {
@@ -10455,9 +10666,9 @@ Paths: ` + relatedPaths.join(", ") : ""));
10455
10666
  const verbose = !!params.verbose;
10456
10667
  const dryRun = !!params.dry_run;
10457
10668
  const focus = typeof params.focus === "string" ? params.focus.trim() || undefined : undefined;
10458
- const maxLlmCalls = typeof params.max_calls === "number" && Number.isInteger(params.max_calls) && params.max_calls >= 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;
10669
+ const maxLlmCalls = typeof params.max_calls === "number" && Number.isInteger(params.max_calls) && params.max_calls >= BUDGET_LIMITS.CALLS.min && params.max_calls <= BUDGET_LIMITS.CALLS.max ? params.max_calls : undefined;
10670
+ const maxLlmInputTokens = typeof params.max_input_tokens === "number" && Number.isInteger(params.max_input_tokens) && params.max_input_tokens >= BUDGET_LIMITS.INPUT_TOKENS.min && params.max_input_tokens <= BUDGET_LIMITS.INPUT_TOKENS.max ? params.max_input_tokens : undefined;
10671
+ const maxLatencyMs = typeof params.max_latency_ms === "number" && params.max_latency_ms >= BUDGET_LIMITS.LATENCY_MS.min && params.max_latency_ms <= BUDGET_LIMITS.LATENCY_MS.max ? params.max_latency_ms : undefined;
10461
10672
  if (params.report || params.dashboard) {
10462
10673
  const report = buildMetricsReport();
10463
10674
  const fp = params.dashboard ? writeMetricsDashboard() : null;