pi-smart-compact 8.0.5 → 8.0.7

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