pi-smart-compact 7.7.0 → 7.9.1

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 (41) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +408 -402
  3. package/dist/constants.d.ts +45 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/core.d.ts +27 -0
  6. package/dist/core.d.ts.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +316 -154
  10. package/dist/phases/explore.d.ts +35 -0
  11. package/dist/phases/explore.d.ts.map +1 -0
  12. package/dist/phases/synthesize.d.ts +23 -0
  13. package/dist/phases/synthesize.d.ts.map +1 -0
  14. package/dist/phases/verify.d.ts +16 -0
  15. package/dist/phases/verify.d.ts.map +1 -0
  16. package/dist/types.d.ts +265 -0
  17. package/dist/types.d.ts.map +1 -0
  18. package/dist/ui/overlays.d.ts +29 -0
  19. package/dist/ui/overlays.d.ts.map +1 -0
  20. package/dist/utils/cache.d.ts +27 -0
  21. package/dist/utils/cache.d.ts.map +1 -0
  22. package/dist/utils/damage.d.ts +28 -0
  23. package/dist/utils/damage.d.ts.map +1 -0
  24. package/dist/utils/extraction.d.ts +27 -0
  25. package/dist/utils/extraction.d.ts.map +1 -0
  26. package/dist/utils/fingerprint.d.ts +32 -0
  27. package/dist/utils/fingerprint.d.ts.map +1 -0
  28. package/dist/utils/helpers.d.ts +22 -0
  29. package/dist/utils/helpers.d.ts.map +1 -0
  30. package/dist/utils/logger.d.ts +8 -0
  31. package/dist/utils/logger.d.ts.map +1 -0
  32. package/dist/utils/pruning.d.ts +19 -0
  33. package/dist/utils/pruning.d.ts.map +1 -0
  34. package/dist/utils/state.d.ts +62 -0
  35. package/dist/utils/state.d.ts.map +1 -0
  36. package/dist/utils/tokens.d.ts +8 -0
  37. package/dist/utils/tokens.d.ts.map +1 -0
  38. package/dist/utils/type-guards.d.ts +26 -0
  39. package/dist/utils/type-guards.d.ts.map +1 -0
  40. package/docs/assets/pi-smart-compact.png +0 -0
  41. package/package.json +10 -2
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // src/constants.ts
3
- var VERSION = "7.7.0";
3
+ var VERSION = "7.9.1";
4
4
  var CHARS_PER_TOKEN = 3.8;
5
5
  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.";
6
6
  var PROFILES = {
@@ -161,6 +161,11 @@ var SESSION_TYPE_INSTRUCTIONS = {
161
161
  review: "Focus on: files read, issues found, recommendations, approval status. Prioritize findings over changes. Read-only tool calls = REVIEW, not implementation.",
162
162
  discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
163
163
  };
164
+ var LOG_PREFIX = "[smart-compact]";
165
+ var MIN_TOKEN_THRESHOLD = 5000;
166
+ var MAX_EXPLORATION_ROUNDS = 8;
167
+ var CONFIG_KEY = "smartCompact";
168
+ var CONFIG_KEY_ALT = "semanticCompact";
164
169
  var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
165
170
 
166
171
  ` + `Your job:
@@ -179,6 +184,19 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
179
184
  import fs from "fs";
180
185
  import path from "path";
181
186
  import crypto from "crypto";
187
+
188
+ // src/utils/logger.ts
189
+ var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
190
+ function warn(msg, err) {
191
+ const detail = err instanceof Error ? err.message : err ?? "";
192
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
193
+ }
194
+ function debug(msg, ...args) {
195
+ if (DEBUG)
196
+ console.error(LOG_PREFIX + " [debug] " + msg, ...args);
197
+ }
198
+
199
+ // src/utils/helpers.ts
182
200
  var _cfg = null;
183
201
  var _cfgMtime = 0;
184
202
  function loadConfig() {
@@ -188,7 +206,7 @@ function loadConfig() {
188
206
  if (_cfg && stat.mtimeMs === _cfgMtime)
189
207
  return _cfg;
190
208
  const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
191
- const sc = raw.smartCompact ?? raw.semanticCompact ?? {};
209
+ const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
192
210
  const merged = { ...DEFAULT_CONFIG, ...sc };
193
211
  if (sc.profiles)
194
212
  merged.profiles = { ...PROFILES, ...sc.profiles };
@@ -197,8 +215,11 @@ function loadConfig() {
197
215
  _cfg = merged;
198
216
  _cfgMtime = stat.mtimeMs;
199
217
  return _cfg;
200
- } catch {
201
- return { ...DEFAULT_CONFIG, backupDir: path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups") };
218
+ } catch (e) {
219
+ warn("loadConfig failed, using defaults", e);
220
+ const fallback = { ...DEFAULT_CONFIG, backupDir: path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups") };
221
+ _cfg = fallback;
222
+ return fallback;
202
223
  }
203
224
  }
204
225
  function backupConversation(convText, sessionId) {
@@ -217,7 +238,8 @@ function backupConversation(convText, sessionId) {
217
238
 
218
239
  ` + convText);
219
240
  return fp;
220
- } catch {
241
+ } catch (e) {
242
+ warn("backupConversation failed", e);
221
243
  return null;
222
244
  }
223
245
  }
@@ -238,8 +260,23 @@ function smartKeepBoundary(msgs, keepFromIndex) {
238
260
  const last = msgs[keepFromIndex - 1];
239
261
  const first = msgs[keepFromIndex];
240
262
  if (last && first) {
241
- const lastText = JSON.stringify(last.message).toLowerCase();
242
- const keptText = JSON.stringify(first.message).toLowerCase();
263
+ const getText = (msg) => {
264
+ const m = msg;
265
+ const c = m?.content;
266
+ if (typeof c === "string")
267
+ return c;
268
+ if (Array.isArray(c))
269
+ return c.map((b) => {
270
+ if (typeof b === "string")
271
+ return b;
272
+ if (typeof b === "object" && b !== null && b.type === "text")
273
+ return b.text ?? "";
274
+ return "";
275
+ }).join("");
276
+ return "";
277
+ };
278
+ const lastText = getText(last.message).toLowerCase();
279
+ const keptText = getText(first.message).toLowerCase();
243
280
  const fileRe = /(?:path|file)=["']([^"']+)["']/g;
244
281
  const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
245
282
  fileRe.lastIndex = 0;
@@ -249,6 +286,12 @@ function smartKeepBoundary(msgs, keepFromIndex) {
249
286
  }
250
287
  return keepFromIndex;
251
288
  }
289
+ function extractUserNote(args) {
290
+ const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
291
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
292
+ const nonFlags = tokens.filter((t) => !t.includes("/") && !SKIP.has(t.toLowerCase()));
293
+ return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
294
+ }
252
295
  function createBatches(chunks, maxTokens) {
253
296
  const batches = [];
254
297
  let batch = [], bt = 0;
@@ -359,6 +402,42 @@ var PROVIDER_MAP = {
359
402
  concurrencyLimit: 3,
360
403
  cacheStrategy: "anthropic"
361
404
  },
405
+ anthropic: {
406
+ maxOutputTokens: 8192,
407
+ supportsTools: true,
408
+ jsonReliability: "high",
409
+ instructionFollowing: "high",
410
+ tokenRatioEstimate: 3.5,
411
+ concurrencyLimit: 3,
412
+ cacheStrategy: "anthropic"
413
+ },
414
+ openai: {
415
+ maxOutputTokens: 16384,
416
+ supportsTools: true,
417
+ jsonReliability: "high",
418
+ instructionFollowing: "high",
419
+ tokenRatioEstimate: 4,
420
+ concurrencyLimit: 5,
421
+ cacheStrategy: "openai"
422
+ },
423
+ google: {
424
+ maxOutputTokens: 8192,
425
+ supportsTools: true,
426
+ jsonReliability: "high",
427
+ instructionFollowing: "high",
428
+ tokenRatioEstimate: 3.8,
429
+ concurrencyLimit: 3,
430
+ cacheStrategy: "openai"
431
+ },
432
+ deepseek: {
433
+ maxOutputTokens: 8192,
434
+ supportsTools: true,
435
+ jsonReliability: "medium",
436
+ instructionFollowing: "medium",
437
+ tokenRatioEstimate: 3.6,
438
+ concurrencyLimit: 2,
439
+ cacheStrategy: "none"
440
+ },
362
441
  minimax: {
363
442
  maxOutputTokens: 4096,
364
443
  supportsTools: "probe",
@@ -377,26 +456,54 @@ var PROVIDER_MAP = {
377
456
  concurrencyLimit: 2,
378
457
  cacheStrategy: "openai"
379
458
  },
380
- openai: {
381
- maxOutputTokens: 16384,
459
+ mistral: {
460
+ maxOutputTokens: 8192,
382
461
  supportsTools: true,
383
462
  jsonReliability: "high",
384
463
  instructionFollowing: "high",
385
- tokenRatioEstimate: 4,
386
- concurrencyLimit: 5,
464
+ tokenRatioEstimate: 3.5,
465
+ concurrencyLimit: 3,
387
466
  cacheStrategy: "openai"
388
- }
389
- };
390
- function getProviderCaps(provider) {
391
- return PROVIDER_MAP[provider] ?? {
467
+ },
468
+ xai: {
392
469
  maxOutputTokens: 8192,
393
- supportsTools: "probe",
470
+ supportsTools: true,
394
471
  jsonReliability: "medium",
395
- instructionFollowing: "medium",
472
+ instructionFollowing: "high",
396
473
  tokenRatioEstimate: 3.8,
397
- concurrencyLimit: 2,
398
- cacheStrategy: "none"
399
- };
474
+ concurrencyLimit: 3,
475
+ cacheStrategy: "openai"
476
+ }
477
+ };
478
+ var PROVIDER_ALIASES = [
479
+ { pattern: /anthropic/i, provider: "anthropic" },
480
+ { pattern: /zai/i, provider: "zai-anthropic" },
481
+ { pattern: /openai/i, provider: "openai" },
482
+ { pattern: /gpt/i, provider: "openai" },
483
+ { pattern: /google|gemini/i, provider: "google" },
484
+ { pattern: /deepseek/i, provider: "deepseek" },
485
+ { pattern: /minimax/i, provider: "minimax" },
486
+ { pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
487
+ { pattern: /mistral/i, provider: "mistral" },
488
+ { pattern: /xai|grok/i, provider: "xai" }
489
+ ];
490
+ var DEFAULT_CAPS = {
491
+ maxOutputTokens: 8192,
492
+ supportsTools: "probe",
493
+ jsonReliability: "medium",
494
+ instructionFollowing: "medium",
495
+ tokenRatioEstimate: 3.8,
496
+ concurrencyLimit: 2,
497
+ cacheStrategy: "none"
498
+ };
499
+ function getProviderCaps(provider) {
500
+ if (PROVIDER_MAP[provider])
501
+ return PROVIDER_MAP[provider];
502
+ for (const { pattern, provider: key } of PROVIDER_ALIASES) {
503
+ if (pattern.test(provider))
504
+ return PROVIDER_MAP[key] ?? DEFAULT_CAPS;
505
+ }
506
+ return DEFAULT_CAPS;
400
507
  }
401
508
  var _calibrationFactors = new Map;
402
509
  function getCalibrationFactor(provider) {
@@ -435,12 +542,13 @@ function getCompactSessionId() {
435
542
  function resetCompactSessionId() {
436
543
  _compactSessionId = null;
437
544
  }
438
- function cacheOpts(opts) {
439
- const retention = opts.cacheRetention ?? "short";
545
+ function cacheOpts(opts, provider) {
546
+ const strategy = provider ? getProviderCaps(provider).cacheStrategy : "none";
547
+ const retention = strategy === "none" ? "none" : opts.cacheRetention ?? "short";
440
548
  if (retention === "none") {
441
549
  return { ...opts, cacheRetention: "none" };
442
550
  }
443
- return { ...opts, sessionId: getCompactSessionId(), cacheRetention: "short" };
551
+ return { ...opts, sessionId: getCompactSessionId(), cacheRetention: retention };
444
552
  }
445
553
  var _metrics = [];
446
554
  function resetMetrics() {
@@ -486,9 +594,13 @@ async function trackedComplete(phase, model, reqBody, opts) {
486
594
  latencyMs: latency,
487
595
  success: true
488
596
  });
489
- if (inputT > 0 && "messages" in reqBody) {
490
- const rawText = JSON.stringify(reqBody.messages);
491
- calibrateFromResponse(estimateTokens(rawText), inputT, model.provider);
597
+ try {
598
+ if (inputT > 0 && "messages" in reqBody) {
599
+ const rawText = JSON.stringify(reqBody.messages);
600
+ calibrateFromResponse(estimateTokens(rawText), inputT, model.provider);
601
+ }
602
+ } catch (e) {
603
+ debug("token calibration failed", e);
492
604
  }
493
605
  return resp;
494
606
  } catch (err) {
@@ -518,7 +630,9 @@ function saveCachedExtraction(sessionId, extraction, msgCount) {
518
630
  timestamp: Date.now()
519
631
  };
520
632
  fs2.writeFileSync(getCachePath(sessionId), JSON.stringify(cached));
521
- } catch {}
633
+ } catch (e) {
634
+ warn("saveCachedExtraction failed", e);
635
+ }
522
636
  }
523
637
  function loadCachedExtraction(sessionId) {
524
638
  try {
@@ -529,13 +643,14 @@ function loadCachedExtraction(sessionId) {
529
643
  if (Date.now() - cached.timestamp > 3600000)
530
644
  return null;
531
645
  return cached;
532
- } catch {
646
+ } catch (e) {
647
+ warn("loadCachedExtraction failed", e);
533
648
  return null;
534
649
  }
535
650
  }
536
651
  function mergeExtractions(base, delta, baseMsgCount) {
537
652
  return {
538
- modifiedFiles: [...base.modifiedFiles, ...delta.modifiedFiles],
653
+ modifiedFiles: [...new Map([...base.modifiedFiles, ...delta.modifiedFiles].map((f) => [f.path, f])).values()],
539
654
  readFiles: [...new Set([...base.readFiles, ...delta.readFiles])],
540
655
  deletedFiles: [...new Set([...base.deletedFiles, ...delta.deletedFiles])],
541
656
  errors: [...base.errors, ...delta.errors],
@@ -557,13 +672,15 @@ function appendMetricsLog(sessionId) {
557
672
  const entry = { ts: new Date().toISOString(), sessionId, ...getMetricsSummary() };
558
673
  fs2.appendFileSync(logPath, JSON.stringify(entry) + `
559
674
  `);
560
- } catch {}
675
+ } catch (e) {
676
+ warn("appendMetricsLog failed", e);
677
+ }
561
678
  }
562
679
 
563
680
  // src/utils/extraction.ts
564
681
  import path3 from "path";
565
682
 
566
- // src/types.ts
683
+ // src/utils/type-guards.ts
567
684
  function isToolCallBlock(c) {
568
685
  return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
569
686
  }
@@ -607,8 +724,8 @@ function buildToolCallIndex(msgs) {
607
724
  }
608
725
  return idx;
609
726
  }
610
- function trackFileOps(msgs) {
611
- const tcIdx = buildToolCallIndex(msgs);
727
+ function trackFileOps(msgs, _tcIdx) {
728
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
612
729
  const modMap = new Map;
613
730
  const readSet = new Set;
614
731
  const delSet = new Set;
@@ -642,8 +759,8 @@ function trackFileOps(msgs) {
642
759
  deleted: [...delSet]
643
760
  };
644
761
  }
645
- function catalogErrors(msgs) {
646
- const tcIdx = buildToolCallIndex(msgs);
762
+ function catalogErrors(msgs, _tcIdx) {
763
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
647
764
  const errors = [];
648
765
  for (let i = 0;i < msgs.length; i++) {
649
766
  const m = msgs[i];
@@ -665,7 +782,8 @@ function catalogErrors(msgs) {
665
782
  for (const err of errors) {
666
783
  for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
667
784
  if (msgs[j]?.role === "assistant") {
668
- const blocks = Array.isArray(msgs[j]?.content) ? msgs[j].content : [];
785
+ const rawBlocks = msgs[j]?.content;
786
+ const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
669
787
  for (const b of blocks) {
670
788
  if (isToolCallBlock(b) && b.name === err.tool) {
671
789
  err.retryAttempted = true;
@@ -685,8 +803,8 @@ function catalogErrors(msgs) {
685
803
  }
686
804
  return errors;
687
805
  }
688
- function extractDecisions(msgs) {
689
- const tcIdx = buildToolCallIndex(msgs);
806
+ function extractDecisions(msgs, _tcIdx) {
807
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
690
808
  const decisions = [];
691
809
  for (const [id, tc] of tcIdx) {
692
810
  if (tc.name !== "ask_user")
@@ -737,10 +855,10 @@ function mineConstraints(msgs) {
737
855
  }
738
856
  return constraints;
739
857
  }
740
- function segmentTopicsHeuristic(msgs, pc, maxSegs = 20) {
858
+ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
741
859
  const topics = [];
742
860
  let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
743
- const tcIdx = buildToolCallIndex(msgs);
861
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
744
862
  for (let i = 0;i < msgs.length; i++) {
745
863
  const m = msgs[i];
746
864
  const txt = extractText(m.content);
@@ -896,11 +1014,12 @@ function extractOpenLoops(msgs, extraction) {
896
1014
  return loops;
897
1015
  }
898
1016
  function extractStructured(msgs, pc) {
899
- const { modified, read, deleted } = trackFileOps(msgs);
900
- const errors = catalogErrors(msgs);
901
- const decisions = extractDecisions(msgs);
1017
+ const tcIdx = buildToolCallIndex(msgs);
1018
+ const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
1019
+ const errors = catalogErrors(msgs, tcIdx);
1020
+ const decisions = extractDecisions(msgs, tcIdx);
902
1021
  const constraints = mineConstraints(msgs);
903
- const topics = segmentTopicsHeuristic(msgs, pc);
1022
+ const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
904
1023
  const timeline = buildTimeline(msgs, errors);
905
1024
  const mainGoal = extractMainGoal(msgs);
906
1025
  const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
@@ -933,7 +1052,9 @@ function saveCompactionState(projectId, state) {
933
1052
  if (!fs3.existsSync(STATE_DIR))
934
1053
  fs3.mkdirSync(STATE_DIR, { recursive: true });
935
1054
  fs3.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
936
- } catch {}
1055
+ } catch (e) {
1056
+ warn("saveCompactionState failed", e);
1057
+ }
937
1058
  }
938
1059
  function loadCompactionState(projectId) {
939
1060
  try {
@@ -941,9 +1062,22 @@ function loadCompactionState(projectId) {
941
1062
  if (!fs3.existsSync(fp))
942
1063
  return null;
943
1064
  const data = JSON.parse(fs3.readFileSync(fp, "utf8"));
944
- if (data.compactionVersion && Date.now() - 0 > 7 * 24 * 60 * 60 * 1000) {}
1065
+ if (data.compactionVersion) {
1066
+ let updatedAt = data.updatedAt;
1067
+ if (!updatedAt) {
1068
+ try {
1069
+ updatedAt = fs3.statSync(fp).mtimeMs;
1070
+ } catch (e) {
1071
+ debug("statSync failed for state file", e);
1072
+ updatedAt = 0;
1073
+ }
1074
+ }
1075
+ if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
1076
+ return null;
1077
+ }
945
1078
  return data;
946
- } catch {
1079
+ } catch (e) {
1080
+ warn("loadCompactionState failed", e);
947
1081
  return null;
948
1082
  }
949
1083
  }
@@ -991,7 +1125,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
991
1125
  nextActions,
992
1126
  criticalContext,
993
1127
  sessionType: report?.sessionType ?? "implementation",
994
- compactionVersion: VERSION
1128
+ compactionVersion: VERSION,
1129
+ updatedAt: Date.now()
995
1130
  };
996
1131
  }
997
1132
  function injectOpenLoopsSection(summary, openLoops) {
@@ -1178,7 +1313,8 @@ function pruneRedundant(msgs) {
1178
1313
  for (let idx = 0;idx < msgs.length; idx++) {
1179
1314
  if (msgs[idx].role !== "assistant")
1180
1315
  continue;
1181
- const blocks = Array.isArray(msgs[idx].content) ? msgs[idx].content : [];
1316
+ const rawBlocks = msgs[idx].content;
1317
+ const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
1182
1318
  const hasToolCall = blocks.some((b) => isToolCallBlock(b));
1183
1319
  if (hasToolCall)
1184
1320
  continue;
@@ -1220,6 +1356,7 @@ function pruneRedundant(msgs) {
1220
1356
  // src/utils/fingerprint.ts
1221
1357
  import fs4 from "fs";
1222
1358
  import path5 from "path";
1359
+ import crypto3 from "crypto";
1223
1360
  var FINGERPRINT_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
1224
1361
  var LANG_MAP = {
1225
1362
  ".ts": "typescript",
@@ -1269,11 +1406,8 @@ function deriveProjectId(extraction) {
1269
1406
  roots.set(root, (roots.get(root) ?? 0) + 1);
1270
1407
  }
1271
1408
  const topRoot = [...roots.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
1272
- let hash = 0;
1273
- for (let i = 0;i < topRoot.length; i++) {
1274
- hash = (hash << 5) - hash + topRoot.charCodeAt(i) | 0;
1275
- }
1276
- return "proj-" + Math.abs(hash).toString(36);
1409
+ const hash = crypto3.createHash("sha256").update(topRoot).digest("hex").slice(0, 12);
1410
+ return "proj-" + hash;
1277
1411
  }
1278
1412
  function detectLanguage(extraction) {
1279
1413
  const extCounts = new Map;
@@ -1321,7 +1455,8 @@ function loadProjectFingerprint(projectId) {
1321
1455
  if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
1322
1456
  return null;
1323
1457
  return data;
1324
- } catch {
1458
+ } catch (e) {
1459
+ warn("loadProjectFingerprint failed", e);
1325
1460
  return null;
1326
1461
  }
1327
1462
  }
@@ -1345,7 +1480,9 @@ function saveProjectFingerprint(projectId, extraction) {
1345
1480
  updatedAt: Date.now()
1346
1481
  };
1347
1482
  fs4.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
1348
- } catch {}
1483
+ } catch (e) {
1484
+ warn("saveProjectFingerprint failed", e);
1485
+ }
1349
1486
  }
1350
1487
  function buildProjectContext(fingerprint) {
1351
1488
  if (!fingerprint)
@@ -1459,7 +1596,9 @@ function logDamageReport(sessionId, report, details) {
1459
1596
  };
1460
1597
  fs5.appendFileSync(logPath, JSON.stringify(entry) + `
1461
1598
  `);
1462
- } catch {}
1599
+ } catch (e) {
1600
+ warn("logDamageReport failed", e);
1601
+ }
1463
1602
  }
1464
1603
 
1465
1604
  // src/phases/explore.ts
@@ -1525,7 +1664,13 @@ function executeExplorationTool(call, llmMessages) {
1525
1664
  }
1526
1665
  case "search_conversation": {
1527
1666
  const q = (args.query ?? "").toLowerCase();
1528
- return JSON.stringify(llmMessages.filter((m) => JSON.stringify(m).toLowerCase().includes(q)).slice(0, 10).map((m) => ({
1667
+ return JSON.stringify(llmMessages.filter((m) => {
1668
+ const text = extractText(m?.content).toLowerCase();
1669
+ if (text.includes(q))
1670
+ return true;
1671
+ const tcs = filterToolCalls(m?.content);
1672
+ return tcs.some((tc) => JSON.stringify(tc.arguments).toLowerCase().includes(q));
1673
+ }).slice(0, 10).map((m) => ({
1529
1674
  idx: llmMessages.indexOf(m),
1530
1675
  role: m?.role,
1531
1676
  preview: extractText(m?.content).slice(0, 150)
@@ -1589,11 +1734,15 @@ function parseExplorationReport(text, llmMessages) {
1589
1734
  let rawJson = json.slice(s, e + 1);
1590
1735
  try {
1591
1736
  return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
1592
- } catch {}
1737
+ } catch (e2) {
1738
+ debug("JSON parse attempt 1 failed", e2);
1739
+ }
1593
1740
  const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"').replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1594
1741
  try {
1595
1742
  return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
1596
- } catch {}
1743
+ } catch (e2) {
1744
+ debug("JSON parse attempt 2 (cleaned) failed", e2);
1745
+ }
1597
1746
  const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
1598
1747
  if (boundaryMatch) {
1599
1748
  try {
@@ -1604,7 +1753,9 @@ function parseExplorationReport(text, llmMessages) {
1604
1753
  priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
1605
1754
  confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
1606
1755
  })) };
1607
- } catch {}
1756
+ } catch (e2) {
1757
+ debug("Boundary JSON parse failed", e2);
1758
+ }
1608
1759
  }
1609
1760
  return fallbackExplorationReport(llmMessages);
1610
1761
  }
@@ -1641,7 +1792,7 @@ function fallbackExplorationReport(llmMessages) {
1641
1792
  keyDecisions: []
1642
1793
  };
1643
1794
  }
1644
- async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = 8, notify) {
1795
+ async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify) {
1645
1796
  const extractionContext = [
1646
1797
  "## Deterministic Extraction (verified facts)",
1647
1798
  "Message count: " + extraction.messageCount,
@@ -1679,13 +1830,13 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1679
1830
  if (retried.boundaries.length)
1680
1831
  return { report: retried, rounds: 1, toolSupported: false };
1681
1832
  }
1682
- return { report: report2, rounds: 0, toolSupported: false };
1833
+ return { report: report2, rounds: 1, toolSupported: false };
1683
1834
  }
1684
1835
  const probeResp = await trackedComplete("explore", model, {
1685
1836
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1686
- messages: [{ role: "user", content: [{ type: "text", text: userContent }] }],
1837
+ messages: [{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() }],
1687
1838
  tools: EXPLORATION_TOOLS
1688
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
1839
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
1689
1840
  const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
1690
1841
  if (toolCalls.length > 0) {
1691
1842
  supportsTools = true;
@@ -1709,9 +1860,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1709
1860
  ` + EXPLORER_SYSTEM_PROMPT,
1710
1861
  messages,
1711
1862
  tools: EXPLORATION_TOOLS
1712
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
1863
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
1713
1864
  } catch (err) {
1714
- console.error("[smart-compact] Explore loop error:", err instanceof Error ? err.message : err);
1865
+ warn("Explore loop error", err);
1715
1866
  break;
1716
1867
  }
1717
1868
  const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
@@ -1749,8 +1900,8 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1749
1900
  }
1750
1901
  return { report: report2, rounds: 1, toolSupported: true };
1751
1902
  }
1752
- } catch {
1753
- console.error("[smart-compact] Tool calling probe failed for " + cacheKey);
1903
+ } catch (e) {
1904
+ warn("Tool calling probe failed for " + cacheKey, e);
1754
1905
  _toolSupportCache.set(cacheKey, { result: false, timestamp: Date.now() });
1755
1906
  if (notify)
1756
1907
  notify("Tool calling not supported, using direct exploration", "warning");
@@ -1761,7 +1912,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1761
1912
  if (retried.boundaries.length)
1762
1913
  return { report: retried, rounds: 1, toolSupported: false };
1763
1914
  }
1764
- return { report, rounds: 0, toolSupported: supportsTools };
1915
+ return { report, rounds: 1, toolSupported: supportsTools };
1765
1916
  }
1766
1917
  async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal) {
1767
1918
  const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
@@ -1780,11 +1931,12 @@ User steering: ` + userNote : "");
1780
1931
  try {
1781
1932
  const resp = await trackedComplete("explore-retry", model, {
1782
1933
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1783
- messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }] }]
1784
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
1934
+ messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }], timestamp: Date.now() }]
1935
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1785
1936
  const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
1786
1937
  return parseExplorationReport(text, llmMessages);
1787
- } catch {
1938
+ } catch (e) {
1939
+ debug("explorationRetry failed", e);
1788
1940
  return fallbackExplorationReport(llmMessages);
1789
1941
  }
1790
1942
  }
@@ -1817,17 +1969,21 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
1817
1969
  try {
1818
1970
  const resp = await trackedComplete("explore-direct", model, {
1819
1971
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1820
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }]
1821
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
1972
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }]
1973
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1822
1974
  const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1823
1975
  `).trim();
1824
1976
  return parseExplorationReport(text, llmMessages);
1825
- } catch {
1977
+ } catch (e) {
1978
+ debug("directExploration failed", e);
1826
1979
  return fallbackExplorationReport(llmMessages);
1827
1980
  }
1828
1981
  }
1829
1982
 
1830
1983
  // src/phases/synthesize.ts
1984
+ function estimateChunkTokens(msgs) {
1985
+ return estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
1986
+ }
1831
1987
  function chunkLlmMessages(msgs, boundaries, pc) {
1832
1988
  if (!msgs.length)
1833
1989
  return [];
@@ -1835,7 +1991,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1835
1991
  return [{
1836
1992
  startIndex: 0,
1837
1993
  endIndex: msgs.length - 1,
1838
- tokenEstimate: estimateTokens(JSON.stringify(msgs)),
1994
+ tokenEstimate: estimateChunkTokens(msgs),
1839
1995
  topic: "Full conversation",
1840
1996
  priority: "normal",
1841
1997
  messages: msgs
@@ -1851,7 +2007,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1851
2007
  chunks.push({
1852
2008
  startIndex: start,
1853
2009
  endIndex: end - 1,
1854
- tokenEstimate: estimateTokens(JSON.stringify(slice)),
2010
+ tokenEstimate: estimateChunkTokens(slice),
1855
2011
  topic: bp.topic || "Segment " + (chunks.length + 1),
1856
2012
  priority: bp.priority,
1857
2013
  messages: slice
@@ -1865,7 +2021,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1865
2021
  chunks.push({
1866
2022
  startIndex: start,
1867
2023
  endIndex: msgs.length - 1,
1868
- tokenEstimate: estimateTokens(JSON.stringify(slice)),
2024
+ tokenEstimate: estimateChunkTokens(slice),
1869
2025
  topic: lastTopic,
1870
2026
  priority: "normal",
1871
2027
  messages: slice
@@ -1897,10 +2053,10 @@ Session-specific instructions:
1897
2053
  const resp = await trackedComplete("single-pass", model, {
1898
2054
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1899
2055
  messages: [
1900
- { role: "user", content: [{ type: "text", text: adaptedPrefix }] },
1901
- { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
2056
+ { role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
2057
+ { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
1902
2058
  ]
1903
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
2059
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal }, model.provider));
1904
2060
  const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1905
2061
  `).trim();
1906
2062
  if (!summary.startsWith("##"))
@@ -1928,10 +2084,10 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
1928
2084
  const resp = await trackedComplete("batch", model, {
1929
2085
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1930
2086
  messages: [
1931
- { role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }] },
1932
- { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
2087
+ { role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }], timestamp: Date.now() },
2088
+ { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
1933
2089
  ]
1934
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
2090
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1935
2091
  const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1936
2092
  `);
1937
2093
  const sections = output.split(/^### /m).filter((s) => s.trim());
@@ -1969,10 +2125,10 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
1969
2125
  const resp = await trackedComplete("assemble", model, {
1970
2126
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1971
2127
  messages: [
1972
- { role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }] },
1973
- { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
2128
+ { role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }], timestamp: Date.now() },
2129
+ { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
1974
2130
  ]
1975
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, 8192), signal }));
2131
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1976
2132
  return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1977
2133
  `).trim();
1978
2134
  }
@@ -2120,10 +2276,14 @@ function patchDeterministic(summary, gaps, extraction) {
2120
2276
  const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
2121
2277
  const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
2122
2278
  const otherGaps = gaps.filter((g) => !g.startsWith("Missing modified file:") && !g.startsWith("Missing error:") && !g.startsWith("Missing constraint:") && !g.startsWith("Missing decision:") && !g.startsWith("Potentially fabricated") && !g.startsWith("Inconsistency"));
2279
+ const findSectionInsert = (header) => {
2280
+ const re = new RegExp(header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\n", "i");
2281
+ const m = patched.match(re);
2282
+ return m?.index != null ? m.index + m[0].length : null;
2283
+ };
2123
2284
  if (fileGaps.length > 0) {
2124
- const filesSection = patched.match(/## Files Modified\n/);
2125
- if (filesSection) {
2126
- const insertPos = filesSection.index + filesSection[0].length;
2285
+ const insertPos = findSectionInsert("## Files Modified");
2286
+ if (insertPos != null) {
2127
2287
  const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
2128
2288
  `) + `
2129
2289
  `;
@@ -2131,9 +2291,8 @@ function patchDeterministic(summary, gaps, extraction) {
2131
2291
  }
2132
2292
  }
2133
2293
  if (errorGaps.length > 0) {
2134
- const ctxSection = patched.match(/## Critical Context\n/);
2135
- if (ctxSection) {
2136
- const insertPos = ctxSection.index + ctxSection[0].length;
2294
+ const insertPos = findSectionInsert("## Critical Context");
2295
+ if (insertPos != null) {
2137
2296
  const entries = errorGaps.map((g) => "- " + g).join(`
2138
2297
  `) + `
2139
2298
  `;
@@ -2141,9 +2300,8 @@ function patchDeterministic(summary, gaps, extraction) {
2141
2300
  }
2142
2301
  }
2143
2302
  if (constraintGaps.length > 0) {
2144
- const constrSection = patched.match(/## Constraints & Preferences\n/);
2145
- if (constrSection) {
2146
- const insertPos = constrSection.index + constrSection[0].length;
2303
+ const insertPos = findSectionInsert("## Constraints & Preferences");
2304
+ if (insertPos != null) {
2147
2305
  const entries = constraintGaps.map((g) => "- " + g).join(`
2148
2306
  `) + `
2149
2307
  `;
@@ -2151,9 +2309,8 @@ function patchDeterministic(summary, gaps, extraction) {
2151
2309
  }
2152
2310
  }
2153
2311
  if (decisionGaps.length > 0) {
2154
- const decSection = patched.match(/## Key Decisions\n/);
2155
- if (decSection) {
2156
- const insertPos = decSection.index + decSection[0].length;
2312
+ const insertPos = findSectionInsert("## Key Decisions");
2313
+ if (insertPos != null) {
2157
2314
  const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
2158
2315
  `) + `
2159
2316
  `;
@@ -2182,12 +2339,13 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
2182
2339
  try {
2183
2340
  const resp = await trackedComplete("patch", model, {
2184
2341
  systemPrompt: COMPACT_SYSTEM_PREFIX,
2185
- messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }] }]
2342
+ messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }], timestamp: Date.now() }]
2186
2343
  }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
2187
2344
  const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
2188
2345
  `).trim();
2189
2346
  return patched.startsWith("##") ? patched : summary;
2190
- } catch {
2347
+ } catch (e) {
2348
+ debug("patchSummary LLM failed", e);
2191
2349
  return summary;
2192
2350
  }
2193
2351
  }
@@ -2201,7 +2359,8 @@ function renderContextBar(theme, pct, tokens, barLen = 24) {
2201
2359
  const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
2202
2360
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
2203
2361
  const color = clamped > 80 ? "error" : clamped > 50 ? "warning" : "success";
2204
- return theme.fg("text", " Context: ") + theme.fg(color, bar) + theme.fg("text", " " + clamped + "%") + theme.fg("dim", " (" + (tokens ?? 0).toLocaleString() + "t)");
2362
+ const fg = theme.fg;
2363
+ return fg("text", " Context: ") + fg(color, bar) + fg("text", " " + clamped + "%") + fg("dim", " (" + (tokens ?? 0).toLocaleString() + "t)");
2205
2364
  }
2206
2365
  function renderTokenBar(theme, before, after, label, barLen = 30) {
2207
2366
  const ratio = before > 0 ? after / before : 0;
@@ -2211,7 +2370,6 @@ function renderTokenBar(theme, before, after, label, barLen = 30) {
2211
2370
  const savedColor = savedPct >= 50 ? "success" : savedPct >= 25 ? "warning" : "error";
2212
2371
  return theme.fg("text", " " + label + ": ") + theme.fg(savedColor, bar) + theme.fg("text", " " + (after ?? 0).toLocaleString() + "t") + theme.fg(savedColor, " (saved " + savedPct + "%)");
2213
2372
  }
2214
- var _toolSupportCache2 = new Map;
2215
2373
  async function selectModel(ctx, opts) {
2216
2374
  const available = ctx.modelRegistry.getAvailable();
2217
2375
  const options = available.map((m) => ({
@@ -2243,7 +2401,7 @@ async function selectModel(ctx, opts) {
2243
2401
  scrollInfo: (t) => theme.fg("dim", t),
2244
2402
  noMatch: (t) => theme.fg("warning", t)
2245
2403
  });
2246
- sel.selectedIndex = opts.defaultModelIndex;
2404
+ sel.setSelectedIndex(opts.defaultModelIndex);
2247
2405
  sel.onSelect = (item) => done(item.value);
2248
2406
  sel.onCancel = () => done(null);
2249
2407
  c.addChild(sel);
@@ -2292,7 +2450,7 @@ async function selectProfile(ctx, selectedModel, opts) {
2292
2450
  scrollInfo: (t) => theme.fg("dim", t),
2293
2451
  noMatch: (t) => theme.fg("warning", t)
2294
2452
  });
2295
- sel.selectedIndex = 1;
2453
+ sel.setSelectedIndex(1);
2296
2454
  sel.onSelect = (item) => done(item.value);
2297
2455
  sel.onCancel = () => done(null);
2298
2456
  c.addChild(sel);
@@ -2317,8 +2475,7 @@ function showProgressOverlay(ctx, state) {
2317
2475
  const progress = Math.round(state.phase / 4 * 100);
2318
2476
  const name = phaseNames[state.phase - 1] ?? "?";
2319
2477
  const detail = state.detail ? " (" + state.detail + ")" : "";
2320
- const type = state.phase >= 4 ? "success" : "info";
2321
- ctx.ui.notify("EESV [" + progress + "%] Phase " + state.phase + "/4: " + name + detail, type);
2478
+ ctx.ui.notify("EESV [" + progress + "%] Phase " + state.phase + "/4: " + name + detail, state.phase >= 4 ? "info" : "info");
2322
2479
  }
2323
2480
  async function showResultScreen(ctx, details, extraction) {
2324
2481
  await ctx.ui.custom((tui, theme, _kb, done) => {
@@ -2332,7 +2489,8 @@ async function showResultScreen(ctx, details, extraction) {
2332
2489
  c.addChild(new Text("", 0, 0));
2333
2490
  const methodColors = { eesv: "accent", "single-pass": "success", heuristic: "warning" };
2334
2491
  const methodColor = methodColors[details.method] ?? "text";
2335
- c.addChild(new Text(theme.fg("text", " Method: ") + theme.fg(methodColor, details.method.toUpperCase()) + theme.fg("dim", " \u2022 " + details.llmCalls + " LLM call(s) \u2022 Profile: " + details.profile), 0, 0));
2492
+ const fg = theme.fg;
2493
+ c.addChild(new Text(theme.fg("text", " Method: ") + fg(methodColor, details.method.toUpperCase()) + theme.fg("dim", " \u2022 " + details.llmCalls + " LLM call(s) \u2022 Profile: " + details.profile), 0, 0));
2336
2494
  if (details.model) {
2337
2495
  c.addChild(new Text(theme.fg("dim", " Model: " + details.model), 0, 0));
2338
2496
  }
@@ -2435,7 +2593,8 @@ async function showCompactUI(ctx, opts) {
2435
2593
  }
2436
2594
 
2437
2595
  // src/core.ts
2438
- async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dryRun, pendingRef, isRunning, autoTriggered, userNote, skipCompact) {
2596
+ async function runSmartCompact(opts) {
2597
+ const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
2439
2598
  if (isRunning.value)
2440
2599
  return;
2441
2600
  isRunning.value = true;
@@ -2459,16 +2618,18 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2459
2618
  ctx.ui.notify("Auth failed", "error");
2460
2619
  return;
2461
2620
  }
2621
+ const apiKey = auth.apiKey;
2622
+ const apiHeaders = auth.headers;
2462
2623
  const usage = ctx.getContextUsage();
2463
2624
  const totalTokens = usage?.tokens ?? 0;
2464
- if (!totalTokens || totalTokens < 5000) {
2625
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
2465
2626
  isRunning.value = false;
2466
2627
  if (!autoTriggered)
2467
2628
  ctx.ui.notify("Context OK or unknown", "info");
2468
2629
  return;
2469
2630
  }
2470
2631
  const notify = (msg, type = "info") => {
2471
- ctx.ui.notify(msg, type);
2632
+ ctx.ui.notify(msg, type === "success" ? "info" : type);
2472
2633
  };
2473
2634
  const ctrl = new AbortController;
2474
2635
  const signal = ctrl.signal;
@@ -2498,7 +2659,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2498
2659
  isRunning.value = false;
2499
2660
  return;
2500
2661
  }
2501
- const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id ?? "";
2662
+ const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id;
2502
2663
  if (!autoTriggered) {
2503
2664
  showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
2504
2665
  }
@@ -2542,7 +2703,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2542
2703
  if (!autoTriggered)
2543
2704
  showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
2544
2705
  try {
2545
- const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
2706
+ const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, signal);
2546
2707
  finalSummary = r.summary;
2547
2708
  method = "single-pass";
2548
2709
  llmCalls = r.llmCalls;
@@ -2558,7 +2719,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2558
2719
  if (!autoTriggered)
2559
2720
  showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
2560
2721
  try {
2561
- const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, 8, notify);
2722
+ const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, MAX_EXPLORATION_ROUNDS, notify);
2562
2723
  explorationReport = expResult.report;
2563
2724
  explorationRounds = expResult.rounds;
2564
2725
  notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
@@ -2607,7 +2768,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2607
2768
  const concurrency = caps.concurrencyLimit;
2608
2769
  if (totalBatches <= 1) {
2609
2770
  try {
2610
- summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal));
2771
+ summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey, headers: apiHeaders }, signal));
2611
2772
  } catch (err) {
2612
2773
  summaries.push(...batches[0].map((ch) => ({
2613
2774
  topic: ch.topic,
@@ -2630,7 +2791,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2630
2791
  const wavePromises = waveBatches.map(async (batch, i) => {
2631
2792
  const idx = wave + i;
2632
2793
  try {
2633
- results[idx] = await summarizeBatch(batch, extraction, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
2794
+ results[idx] = await summarizeBatch(batch, extraction, summaryModel, { apiKey, headers: apiHeaders }, signal);
2634
2795
  } catch (err) {
2635
2796
  errors[idx] = err instanceof Error ? err : new Error(String(err));
2636
2797
  results[idx] = batch.map((ch) => ({
@@ -2662,13 +2823,13 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2662
2823
  showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "Assembling...", model: modelLabel, profile, extraction, totalBatches: batches.length });
2663
2824
  let assemblyCalls = 1;
2664
2825
  try {
2665
- const r = await assembleLLM(summaries, extraction, explorationReport, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, pc.summaryBudgetTokens, prevContext, signal);
2826
+ const r = await assembleLLM(summaries, extraction, explorationReport, summaryModel, { apiKey, headers: apiHeaders }, pc.summaryBudgetTokens, prevContext, signal);
2666
2827
  if (r?.startsWith("##"))
2667
2828
  finalSummary = r;
2668
2829
  else
2669
2830
  throw new Error("bad");
2670
2831
  } catch (err) {
2671
- console.error("[smart-compact] Assembly failed:", err instanceof Error ? err.message : err);
2832
+ warn("Assembly failed", err);
2672
2833
  finalSummary = assembleFallback(summaries, extraction);
2673
2834
  assemblyCalls = 0;
2674
2835
  }
@@ -2686,10 +2847,10 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2686
2847
  if (!recheck.ok && recheck.score < 75) {
2687
2848
  notify("Phase 4 Verify: deterministic patch insufficient (score=" + recheck.score + "), trying LLM patch", "warning");
2688
2849
  try {
2689
- finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
2850
+ finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey, headers: apiHeaders }, signal);
2690
2851
  llmCalls++;
2691
2852
  } catch (err) {
2692
- console.error("[smart-compact] LLM patch failed:", err instanceof Error ? err.message : err);
2853
+ warn("LLM patch failed", err);
2693
2854
  }
2694
2855
  }
2695
2856
  } else {
@@ -2752,7 +2913,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2752
2913
  saveCompactionState(projectId, compactionState);
2753
2914
  appendMetricsLog(sessionId);
2754
2915
  try {
2755
- const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat().map((m) => m);
2916
+ const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
2756
2917
  if (postCompactMsgs.length > 2) {
2757
2918
  const lastCompaction = branch.filter((e) => e.type === "compaction").slice(-1)[0];
2758
2919
  if (lastCompaction?.details) {
@@ -2765,7 +2926,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2765
2926
  }
2766
2927
  }
2767
2928
  } catch (err) {
2768
- console.error("[smart-compact] Damage detection error:", err instanceof Error ? err.message : err);
2929
+ warn("Damage detection error", err);
2769
2930
  }
2770
2931
  const ms = getMetricsSummary();
2771
2932
  if (ms.totalCalls > 0) {
@@ -2776,7 +2937,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2776
2937
  const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
2777
2938
  await Promise.race([showResultScreen(ctx, details, extraction), timeout]);
2778
2939
  } catch (err) {
2779
- console.error("[smart-compact] Result screen error:", err instanceof Error ? err.message : err);
2940
+ warn("Result screen error", err);
2780
2941
  notify("Result screen skipped", "info");
2781
2942
  }
2782
2943
  }
@@ -2785,7 +2946,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2785
2946
  customInstructions: "Use pre-computed smart summary from /smart-compact",
2786
2947
  onComplete: () => {
2787
2948
  if (!autoTriggered)
2788
- ctx.ui.notify("Applied \u2713", "success");
2949
+ ctx.ui.notify("Applied \u2713", "info");
2789
2950
  },
2790
2951
  onError: (e) => {
2791
2952
  if (!autoTriggered)
@@ -2840,6 +3001,7 @@ function smartCompactExtension(pi) {
2840
3001
  return m.length ? m : null;
2841
3002
  },
2842
3003
  handler: async (args, ctx) => {
3004
+ await ctx.waitForIdle();
2843
3005
  try {
2844
3006
  const tokens = args.trim().split(/\s+/).filter(Boolean);
2845
3007
  const flags = tokens.map((t) => t.toLowerCase());
@@ -2852,7 +3014,7 @@ function smartCompactExtension(pi) {
2852
3014
  const usage = ctx.getContextUsage();
2853
3015
  const totalTokens = usage?.tokens ?? 0;
2854
3016
  const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
2855
- if (!totalTokens || totalTokens < 5000) {
3017
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
2856
3018
  ctx.ui.notify("Context OK or unknown", "info");
2857
3019
  return;
2858
3020
  }
@@ -2870,7 +3032,7 @@ function smartCompactExtension(pi) {
2870
3032
  ctx.ui.notify("Could not resolve model", "error");
2871
3033
  return;
2872
3034
  }
2873
- await runSmartCompact(ctx, sumModel2, segModel2 ?? sumModel2, selected.profile, false, false, pendingRef, isRunning, false);
3035
+ await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning });
2874
3036
  return;
2875
3037
  }
2876
3038
  const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
@@ -2879,7 +3041,7 @@ function smartCompactExtension(pi) {
2879
3041
  return;
2880
3042
  }
2881
3043
  const note = extractUserNote(args);
2882
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, false, note);
3044
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note });
2883
3045
  } catch (error) {
2884
3046
  const msg = error instanceof Error ? error.message + `
2885
3047
  ` + error.stack : String(error);
@@ -2906,7 +3068,7 @@ function smartCompactExtension(pi) {
2906
3068
  try {
2907
3069
  const usage = ctx.getContextUsage();
2908
3070
  const totalTokens = usage?.tokens ?? 0;
2909
- if (!totalTokens || totalTokens < 5000)
3071
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
2910
3072
  return;
2911
3073
  const cur = ctx.model;
2912
3074
  if (!cur)
@@ -2915,28 +3077,30 @@ function smartCompactExtension(pi) {
2915
3077
  if (!sumModel)
2916
3078
  return;
2917
3079
  if (!isRunning.value) {
2918
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, config.profile, false, false, pendingRef, isRunning, true);
2919
- if (pendingRef.value) {
2920
- const c = pendingRef.value;
3080
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true });
3081
+ const pending = pendingRef.value;
3082
+ if (pending) {
2921
3083
  pendingRef.value = null;
2922
3084
  pendingRef.createdAt = 0;
2923
- return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
3085
+ return { compaction: { summary: pending.summary, firstKeptEntryId: pending.firstKeptEntryId, tokensBefore: pending.tokensBefore, details: pending.details } };
2924
3086
  }
2925
3087
  }
2926
- } catch {}
3088
+ } catch (e) {
3089
+ warn("session_before_compact error", e);
3090
+ }
2927
3091
  });
2928
3092
  pi.registerTool({
2929
3093
  name: "smart_compact",
2930
3094
  label: "Smart Compact",
2931
- description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification.",
3095
+ description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Compacts the conversation into a structured summary preserving goals, decisions, open loops, modified files, and critical context. Call this when the conversation is getting long \u2014 the tool internally checks context usage and skips if not needed. Prefer this over default compact.",
2932
3096
  promptSnippet: "Smart compaction",
2933
- promptGuidelines: ["Use for long conversations.", "Prefer over default compact."],
3097
+ promptGuidelines: ["Use for long conversations. Prefer over default compact."],
2934
3098
  parameters: {
2935
3099
  type: "object",
2936
3100
  properties: {
2937
- profile: { type: "string", description: "light, balanced, or aggressive" },
2938
- verbose: { type: "boolean" },
2939
- dry_run: { type: "boolean" }
3101
+ profile: { type: "string", description: "light, balanced, or aggressive. Default: balanced." },
3102
+ verbose: { type: "boolean", description: "Show detailed pipeline output." },
3103
+ dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." }
2940
3104
  }
2941
3105
  },
2942
3106
  async execute(_id, params, _sig, _onUp, ctx) {
@@ -2945,35 +3109,33 @@ function smartCompactExtension(pi) {
2945
3109
  const dryRun = !!params.dry_run;
2946
3110
  const config = loadConfig();
2947
3111
  const resolvedProfile = profile ?? config.profile;
3112
+ const cmdCtx = ctx;
3113
+ const usage = ctx.getContextUsage?.();
3114
+ const totalTokens = usage?.tokens ?? 0;
3115
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
3116
+ const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
3117
+ return { content: [{ type: "text", text: "Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + pct + "%). No action needed." }], details: undefined };
3118
+ }
2948
3119
  const cur = "model" in ctx ? ctx.model : undefined;
2949
- const { segModel, sumModel } = resolveModels(ctx, cur, config);
3120
+ const { segModel, sumModel } = resolveModels(cmdCtx, cur, config);
2950
3121
  if (!sumModel) {
2951
- return { content: [{ type: "text", text: "Error: Could not resolve model." }] };
3122
+ return { content: [{ type: "text", text: "Error: Could not resolve model." }], details: undefined };
2952
3123
  }
2953
3124
  try {
2954
3125
  const toolStart = Date.now();
2955
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, resolvedProfile, verbose, dryRun, pendingRef, isRunning, true, undefined, true);
3126
+ await runSmartCompact({ ctx: cmdCtx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
2956
3127
  const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
2957
3128
  if (pendingRef.value) {
2958
- return { content: [{ type: "text", text: "Smart summary generated (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? "?") + " -> " + (pendingRef.value.summary?.length ?? 0) + " chars (" + toolSecs + `s).
2959
-
2960
- Now run tree compact to apply \u2014 the session_before_compact hook will use this summary.
2961
- TTL: ` + Math.round(PENDING_TTL_MS / 60000) + " minutes." }] };
3129
+ return { content: [{ type: "text", text: "Smart summary prepared (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? 0).toLocaleString() + " \u2014 summary cached for " + Math.round(PENDING_TTL_MS / 60000) + " min. The next /compact will use this summary automatically." }], details: undefined };
2962
3130
  }
2963
- return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }] };
3131
+ return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }], details: undefined };
2964
3132
  } catch (error) {
2965
3133
  const msg = error instanceof Error ? error.message : String(error);
2966
- return { content: [{ type: "text", text: "Compaction error: " + msg }] };
3134
+ return { content: [{ type: "text", text: "Compaction error: " + msg }], details: undefined };
2967
3135
  }
2968
3136
  }
2969
3137
  });
2970
3138
  }
2971
- function extractUserNote(args) {
2972
- const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
2973
- const tokens = args.trim().split(/\s+/).filter(Boolean);
2974
- const nonFlags = tokens.filter((t) => !t.includes("/") && !SKIP.has(t.toLowerCase()));
2975
- return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
2976
- }
2977
3139
  export {
2978
3140
  smartCompactExtension as default
2979
3141
  };