pi-smart-compact 7.7.0 → 7.9.0

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 +32 -0
  2. package/README.md +407 -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 +264 -112
  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.0";
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];
@@ -685,8 +802,8 @@ function catalogErrors(msgs) {
685
802
  }
686
803
  return errors;
687
804
  }
688
- function extractDecisions(msgs) {
689
- const tcIdx = buildToolCallIndex(msgs);
805
+ function extractDecisions(msgs, _tcIdx) {
806
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
690
807
  const decisions = [];
691
808
  for (const [id, tc] of tcIdx) {
692
809
  if (tc.name !== "ask_user")
@@ -737,10 +854,10 @@ function mineConstraints(msgs) {
737
854
  }
738
855
  return constraints;
739
856
  }
740
- function segmentTopicsHeuristic(msgs, pc, maxSegs = 20) {
857
+ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
741
858
  const topics = [];
742
859
  let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
743
- const tcIdx = buildToolCallIndex(msgs);
860
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
744
861
  for (let i = 0;i < msgs.length; i++) {
745
862
  const m = msgs[i];
746
863
  const txt = extractText(m.content);
@@ -896,11 +1013,12 @@ function extractOpenLoops(msgs, extraction) {
896
1013
  return loops;
897
1014
  }
898
1015
  function extractStructured(msgs, pc) {
899
- const { modified, read, deleted } = trackFileOps(msgs);
900
- const errors = catalogErrors(msgs);
901
- const decisions = extractDecisions(msgs);
1016
+ const tcIdx = buildToolCallIndex(msgs);
1017
+ const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
1018
+ const errors = catalogErrors(msgs, tcIdx);
1019
+ const decisions = extractDecisions(msgs, tcIdx);
902
1020
  const constraints = mineConstraints(msgs);
903
- const topics = segmentTopicsHeuristic(msgs, pc);
1021
+ const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
904
1022
  const timeline = buildTimeline(msgs, errors);
905
1023
  const mainGoal = extractMainGoal(msgs);
906
1024
  const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
@@ -933,7 +1051,9 @@ function saveCompactionState(projectId, state) {
933
1051
  if (!fs3.existsSync(STATE_DIR))
934
1052
  fs3.mkdirSync(STATE_DIR, { recursive: true });
935
1053
  fs3.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
936
- } catch {}
1054
+ } catch (e) {
1055
+ warn("saveCompactionState failed", e);
1056
+ }
937
1057
  }
938
1058
  function loadCompactionState(projectId) {
939
1059
  try {
@@ -941,9 +1061,22 @@ function loadCompactionState(projectId) {
941
1061
  if (!fs3.existsSync(fp))
942
1062
  return null;
943
1063
  const data = JSON.parse(fs3.readFileSync(fp, "utf8"));
944
- if (data.compactionVersion && Date.now() - 0 > 7 * 24 * 60 * 60 * 1000) {}
1064
+ if (data.compactionVersion) {
1065
+ let updatedAt = data.updatedAt;
1066
+ if (!updatedAt) {
1067
+ try {
1068
+ updatedAt = fs3.statSync(fp).mtimeMs;
1069
+ } catch (e) {
1070
+ debug("statSync failed for state file", e);
1071
+ updatedAt = 0;
1072
+ }
1073
+ }
1074
+ if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
1075
+ return null;
1076
+ }
945
1077
  return data;
946
- } catch {
1078
+ } catch (e) {
1079
+ warn("loadCompactionState failed", e);
947
1080
  return null;
948
1081
  }
949
1082
  }
@@ -991,7 +1124,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
991
1124
  nextActions,
992
1125
  criticalContext,
993
1126
  sessionType: report?.sessionType ?? "implementation",
994
- compactionVersion: VERSION
1127
+ compactionVersion: VERSION,
1128
+ updatedAt: Date.now()
995
1129
  };
996
1130
  }
997
1131
  function injectOpenLoopsSection(summary, openLoops) {
@@ -1220,6 +1354,7 @@ function pruneRedundant(msgs) {
1220
1354
  // src/utils/fingerprint.ts
1221
1355
  import fs4 from "fs";
1222
1356
  import path5 from "path";
1357
+ import crypto3 from "crypto";
1223
1358
  var FINGERPRINT_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
1224
1359
  var LANG_MAP = {
1225
1360
  ".ts": "typescript",
@@ -1269,11 +1404,8 @@ function deriveProjectId(extraction) {
1269
1404
  roots.set(root, (roots.get(root) ?? 0) + 1);
1270
1405
  }
1271
1406
  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);
1407
+ const hash = crypto3.createHash("sha256").update(topRoot).digest("hex").slice(0, 12);
1408
+ return "proj-" + hash;
1277
1409
  }
1278
1410
  function detectLanguage(extraction) {
1279
1411
  const extCounts = new Map;
@@ -1321,7 +1453,8 @@ function loadProjectFingerprint(projectId) {
1321
1453
  if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
1322
1454
  return null;
1323
1455
  return data;
1324
- } catch {
1456
+ } catch (e) {
1457
+ warn("loadProjectFingerprint failed", e);
1325
1458
  return null;
1326
1459
  }
1327
1460
  }
@@ -1345,7 +1478,9 @@ function saveProjectFingerprint(projectId, extraction) {
1345
1478
  updatedAt: Date.now()
1346
1479
  };
1347
1480
  fs4.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
1348
- } catch {}
1481
+ } catch (e) {
1482
+ warn("saveProjectFingerprint failed", e);
1483
+ }
1349
1484
  }
1350
1485
  function buildProjectContext(fingerprint) {
1351
1486
  if (!fingerprint)
@@ -1459,7 +1594,9 @@ function logDamageReport(sessionId, report, details) {
1459
1594
  };
1460
1595
  fs5.appendFileSync(logPath, JSON.stringify(entry) + `
1461
1596
  `);
1462
- } catch {}
1597
+ } catch (e) {
1598
+ warn("logDamageReport failed", e);
1599
+ }
1463
1600
  }
1464
1601
 
1465
1602
  // src/phases/explore.ts
@@ -1525,7 +1662,13 @@ function executeExplorationTool(call, llmMessages) {
1525
1662
  }
1526
1663
  case "search_conversation": {
1527
1664
  const q = (args.query ?? "").toLowerCase();
1528
- return JSON.stringify(llmMessages.filter((m) => JSON.stringify(m).toLowerCase().includes(q)).slice(0, 10).map((m) => ({
1665
+ return JSON.stringify(llmMessages.filter((m) => {
1666
+ const text = extractText(m?.content).toLowerCase();
1667
+ if (text.includes(q))
1668
+ return true;
1669
+ const tcs = filterToolCalls(m?.content);
1670
+ return tcs.some((tc) => JSON.stringify(tc.arguments).toLowerCase().includes(q));
1671
+ }).slice(0, 10).map((m) => ({
1529
1672
  idx: llmMessages.indexOf(m),
1530
1673
  role: m?.role,
1531
1674
  preview: extractText(m?.content).slice(0, 150)
@@ -1589,11 +1732,15 @@ function parseExplorationReport(text, llmMessages) {
1589
1732
  let rawJson = json.slice(s, e + 1);
1590
1733
  try {
1591
1734
  return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
1592
- } catch {}
1735
+ } catch (e2) {
1736
+ debug("JSON parse attempt 1 failed", e2);
1737
+ }
1593
1738
  const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"').replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1594
1739
  try {
1595
1740
  return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
1596
- } catch {}
1741
+ } catch (e2) {
1742
+ debug("JSON parse attempt 2 (cleaned) failed", e2);
1743
+ }
1597
1744
  const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
1598
1745
  if (boundaryMatch) {
1599
1746
  try {
@@ -1604,7 +1751,9 @@ function parseExplorationReport(text, llmMessages) {
1604
1751
  priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
1605
1752
  confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
1606
1753
  })) };
1607
- } catch {}
1754
+ } catch (e2) {
1755
+ debug("Boundary JSON parse failed", e2);
1756
+ }
1608
1757
  }
1609
1758
  return fallbackExplorationReport(llmMessages);
1610
1759
  }
@@ -1641,7 +1790,7 @@ function fallbackExplorationReport(llmMessages) {
1641
1790
  keyDecisions: []
1642
1791
  };
1643
1792
  }
1644
- async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = 8, notify) {
1793
+ async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify) {
1645
1794
  const extractionContext = [
1646
1795
  "## Deterministic Extraction (verified facts)",
1647
1796
  "Message count: " + extraction.messageCount,
@@ -1679,13 +1828,13 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1679
1828
  if (retried.boundaries.length)
1680
1829
  return { report: retried, rounds: 1, toolSupported: false };
1681
1830
  }
1682
- return { report: report2, rounds: 0, toolSupported: false };
1831
+ return { report: report2, rounds: 1, toolSupported: false };
1683
1832
  }
1684
1833
  const probeResp = await trackedComplete("explore", model, {
1685
1834
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1686
1835
  messages: [{ role: "user", content: [{ type: "text", text: userContent }] }],
1687
1836
  tools: EXPLORATION_TOOLS
1688
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
1837
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
1689
1838
  const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
1690
1839
  if (toolCalls.length > 0) {
1691
1840
  supportsTools = true;
@@ -1709,9 +1858,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1709
1858
  ` + EXPLORER_SYSTEM_PROMPT,
1710
1859
  messages,
1711
1860
  tools: EXPLORATION_TOOLS
1712
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
1861
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
1713
1862
  } catch (err) {
1714
- console.error("[smart-compact] Explore loop error:", err instanceof Error ? err.message : err);
1863
+ warn("Explore loop error", err);
1715
1864
  break;
1716
1865
  }
1717
1866
  const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
@@ -1749,8 +1898,8 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1749
1898
  }
1750
1899
  return { report: report2, rounds: 1, toolSupported: true };
1751
1900
  }
1752
- } catch {
1753
- console.error("[smart-compact] Tool calling probe failed for " + cacheKey);
1901
+ } catch (e) {
1902
+ warn("Tool calling probe failed for " + cacheKey, e);
1754
1903
  _toolSupportCache.set(cacheKey, { result: false, timestamp: Date.now() });
1755
1904
  if (notify)
1756
1905
  notify("Tool calling not supported, using direct exploration", "warning");
@@ -1761,7 +1910,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
1761
1910
  if (retried.boundaries.length)
1762
1911
  return { report: retried, rounds: 1, toolSupported: false };
1763
1912
  }
1764
- return { report, rounds: 0, toolSupported: supportsTools };
1913
+ return { report, rounds: 1, toolSupported: supportsTools };
1765
1914
  }
1766
1915
  async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal) {
1767
1916
  const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
@@ -1781,10 +1930,11 @@ User steering: ` + userNote : "");
1781
1930
  const resp = await trackedComplete("explore-retry", model, {
1782
1931
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1783
1932
  messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }] }]
1784
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
1933
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1785
1934
  const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
1786
1935
  return parseExplorationReport(text, llmMessages);
1787
- } catch {
1936
+ } catch (e) {
1937
+ debug("explorationRetry failed", e);
1788
1938
  return fallbackExplorationReport(llmMessages);
1789
1939
  }
1790
1940
  }
@@ -1818,16 +1968,20 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
1818
1968
  const resp = await trackedComplete("explore-direct", model, {
1819
1969
  systemPrompt: COMPACT_SYSTEM_PREFIX,
1820
1970
  messages: [{ role: "user", content: [{ type: "text", text: prompt }] }]
1821
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
1971
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1822
1972
  const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1823
1973
  `).trim();
1824
1974
  return parseExplorationReport(text, llmMessages);
1825
- } catch {
1975
+ } catch (e) {
1976
+ debug("directExploration failed", e);
1826
1977
  return fallbackExplorationReport(llmMessages);
1827
1978
  }
1828
1979
  }
1829
1980
 
1830
1981
  // src/phases/synthesize.ts
1982
+ function estimateChunkTokens(msgs) {
1983
+ return estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
1984
+ }
1831
1985
  function chunkLlmMessages(msgs, boundaries, pc) {
1832
1986
  if (!msgs.length)
1833
1987
  return [];
@@ -1835,7 +1989,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1835
1989
  return [{
1836
1990
  startIndex: 0,
1837
1991
  endIndex: msgs.length - 1,
1838
- tokenEstimate: estimateTokens(JSON.stringify(msgs)),
1992
+ tokenEstimate: estimateChunkTokens(msgs),
1839
1993
  topic: "Full conversation",
1840
1994
  priority: "normal",
1841
1995
  messages: msgs
@@ -1851,7 +2005,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1851
2005
  chunks.push({
1852
2006
  startIndex: start,
1853
2007
  endIndex: end - 1,
1854
- tokenEstimate: estimateTokens(JSON.stringify(slice)),
2008
+ tokenEstimate: estimateChunkTokens(slice),
1855
2009
  topic: bp.topic || "Segment " + (chunks.length + 1),
1856
2010
  priority: bp.priority,
1857
2011
  messages: slice
@@ -1865,7 +2019,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
1865
2019
  chunks.push({
1866
2020
  startIndex: start,
1867
2021
  endIndex: msgs.length - 1,
1868
- tokenEstimate: estimateTokens(JSON.stringify(slice)),
2022
+ tokenEstimate: estimateChunkTokens(slice),
1869
2023
  topic: lastTopic,
1870
2024
  priority: "normal",
1871
2025
  messages: slice
@@ -1900,7 +2054,7 @@ Session-specific instructions:
1900
2054
  { role: "user", content: [{ type: "text", text: adaptedPrefix }] },
1901
2055
  { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
1902
2056
  ]
1903
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
2057
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal }, model.provider));
1904
2058
  const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1905
2059
  `).trim();
1906
2060
  if (!summary.startsWith("##"))
@@ -1931,7 +2085,7 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
1931
2085
  { role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }] },
1932
2086
  { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
1933
2087
  ]
1934
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
2088
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1935
2089
  const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1936
2090
  `);
1937
2091
  const sections = output.split(/^### /m).filter((s) => s.trim());
@@ -1972,7 +2126,7 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
1972
2126
  { role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }] },
1973
2127
  { role: "user", content: [{ type: "text", text: dynamicSuffix }] }
1974
2128
  ]
1975
- }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, 8192), signal }));
2129
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
1976
2130
  return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
1977
2131
  `).trim();
1978
2132
  }
@@ -2120,10 +2274,14 @@ function patchDeterministic(summary, gaps, extraction) {
2120
2274
  const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
2121
2275
  const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
2122
2276
  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"));
2277
+ const findSectionInsert = (header) => {
2278
+ const re = new RegExp(header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\n", "i");
2279
+ const m = patched.match(re);
2280
+ return m?.index != null ? m.index + m[0].length : null;
2281
+ };
2123
2282
  if (fileGaps.length > 0) {
2124
- const filesSection = patched.match(/## Files Modified\n/);
2125
- if (filesSection) {
2126
- const insertPos = filesSection.index + filesSection[0].length;
2283
+ const insertPos = findSectionInsert("## Files Modified");
2284
+ if (insertPos != null) {
2127
2285
  const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
2128
2286
  `) + `
2129
2287
  `;
@@ -2131,9 +2289,8 @@ function patchDeterministic(summary, gaps, extraction) {
2131
2289
  }
2132
2290
  }
2133
2291
  if (errorGaps.length > 0) {
2134
- const ctxSection = patched.match(/## Critical Context\n/);
2135
- if (ctxSection) {
2136
- const insertPos = ctxSection.index + ctxSection[0].length;
2292
+ const insertPos = findSectionInsert("## Critical Context");
2293
+ if (insertPos != null) {
2137
2294
  const entries = errorGaps.map((g) => "- " + g).join(`
2138
2295
  `) + `
2139
2296
  `;
@@ -2141,9 +2298,8 @@ function patchDeterministic(summary, gaps, extraction) {
2141
2298
  }
2142
2299
  }
2143
2300
  if (constraintGaps.length > 0) {
2144
- const constrSection = patched.match(/## Constraints & Preferences\n/);
2145
- if (constrSection) {
2146
- const insertPos = constrSection.index + constrSection[0].length;
2301
+ const insertPos = findSectionInsert("## Constraints & Preferences");
2302
+ if (insertPos != null) {
2147
2303
  const entries = constraintGaps.map((g) => "- " + g).join(`
2148
2304
  `) + `
2149
2305
  `;
@@ -2151,9 +2307,8 @@ function patchDeterministic(summary, gaps, extraction) {
2151
2307
  }
2152
2308
  }
2153
2309
  if (decisionGaps.length > 0) {
2154
- const decSection = patched.match(/## Key Decisions\n/);
2155
- if (decSection) {
2156
- const insertPos = decSection.index + decSection[0].length;
2310
+ const insertPos = findSectionInsert("## Key Decisions");
2311
+ if (insertPos != null) {
2157
2312
  const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
2158
2313
  `) + `
2159
2314
  `;
@@ -2187,7 +2342,8 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
2187
2342
  const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
2188
2343
  `).trim();
2189
2344
  return patched.startsWith("##") ? patched : summary;
2190
- } catch {
2345
+ } catch (e) {
2346
+ debug("patchSummary LLM failed", e);
2191
2347
  return summary;
2192
2348
  }
2193
2349
  }
@@ -2211,7 +2367,6 @@ function renderTokenBar(theme, before, after, label, barLen = 30) {
2211
2367
  const savedColor = savedPct >= 50 ? "success" : savedPct >= 25 ? "warning" : "error";
2212
2368
  return theme.fg("text", " " + label + ": ") + theme.fg(savedColor, bar) + theme.fg("text", " " + (after ?? 0).toLocaleString() + "t") + theme.fg(savedColor, " (saved " + savedPct + "%)");
2213
2369
  }
2214
- var _toolSupportCache2 = new Map;
2215
2370
  async function selectModel(ctx, opts) {
2216
2371
  const available = ctx.modelRegistry.getAvailable();
2217
2372
  const options = available.map((m) => ({
@@ -2435,7 +2590,8 @@ async function showCompactUI(ctx, opts) {
2435
2590
  }
2436
2591
 
2437
2592
  // src/core.ts
2438
- async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dryRun, pendingRef, isRunning, autoTriggered, userNote, skipCompact) {
2593
+ async function runSmartCompact(opts) {
2594
+ const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
2439
2595
  if (isRunning.value)
2440
2596
  return;
2441
2597
  isRunning.value = true;
@@ -2461,7 +2617,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2461
2617
  }
2462
2618
  const usage = ctx.getContextUsage();
2463
2619
  const totalTokens = usage?.tokens ?? 0;
2464
- if (!totalTokens || totalTokens < 5000) {
2620
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
2465
2621
  isRunning.value = false;
2466
2622
  if (!autoTriggered)
2467
2623
  ctx.ui.notify("Context OK or unknown", "info");
@@ -2558,7 +2714,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2558
2714
  if (!autoTriggered)
2559
2715
  showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
2560
2716
  try {
2561
- const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, 8, notify);
2717
+ const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, MAX_EXPLORATION_ROUNDS, notify);
2562
2718
  explorationReport = expResult.report;
2563
2719
  explorationRounds = expResult.rounds;
2564
2720
  notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
@@ -2668,7 +2824,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2668
2824
  else
2669
2825
  throw new Error("bad");
2670
2826
  } catch (err) {
2671
- console.error("[smart-compact] Assembly failed:", err instanceof Error ? err.message : err);
2827
+ warn("Assembly failed", err);
2672
2828
  finalSummary = assembleFallback(summaries, extraction);
2673
2829
  assemblyCalls = 0;
2674
2830
  }
@@ -2689,7 +2845,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2689
2845
  finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
2690
2846
  llmCalls++;
2691
2847
  } catch (err) {
2692
- console.error("[smart-compact] LLM patch failed:", err instanceof Error ? err.message : err);
2848
+ warn("LLM patch failed", err);
2693
2849
  }
2694
2850
  }
2695
2851
  } else {
@@ -2752,7 +2908,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2752
2908
  saveCompactionState(projectId, compactionState);
2753
2909
  appendMetricsLog(sessionId);
2754
2910
  try {
2755
- const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat().map((m) => m);
2911
+ const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
2756
2912
  if (postCompactMsgs.length > 2) {
2757
2913
  const lastCompaction = branch.filter((e) => e.type === "compaction").slice(-1)[0];
2758
2914
  if (lastCompaction?.details) {
@@ -2765,7 +2921,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2765
2921
  }
2766
2922
  }
2767
2923
  } catch (err) {
2768
- console.error("[smart-compact] Damage detection error:", err instanceof Error ? err.message : err);
2924
+ warn("Damage detection error", err);
2769
2925
  }
2770
2926
  const ms = getMetricsSummary();
2771
2927
  if (ms.totalCalls > 0) {
@@ -2776,7 +2932,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
2776
2932
  const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
2777
2933
  await Promise.race([showResultScreen(ctx, details, extraction), timeout]);
2778
2934
  } catch (err) {
2779
- console.error("[smart-compact] Result screen error:", err instanceof Error ? err.message : err);
2935
+ warn("Result screen error", err);
2780
2936
  notify("Result screen skipped", "info");
2781
2937
  }
2782
2938
  }
@@ -2852,7 +3008,7 @@ function smartCompactExtension(pi) {
2852
3008
  const usage = ctx.getContextUsage();
2853
3009
  const totalTokens = usage?.tokens ?? 0;
2854
3010
  const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
2855
- if (!totalTokens || totalTokens < 5000) {
3011
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
2856
3012
  ctx.ui.notify("Context OK or unknown", "info");
2857
3013
  return;
2858
3014
  }
@@ -2870,7 +3026,7 @@ function smartCompactExtension(pi) {
2870
3026
  ctx.ui.notify("Could not resolve model", "error");
2871
3027
  return;
2872
3028
  }
2873
- await runSmartCompact(ctx, sumModel2, segModel2 ?? sumModel2, selected.profile, false, false, pendingRef, isRunning, false);
3029
+ await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning });
2874
3030
  return;
2875
3031
  }
2876
3032
  const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
@@ -2879,7 +3035,7 @@ function smartCompactExtension(pi) {
2879
3035
  return;
2880
3036
  }
2881
3037
  const note = extractUserNote(args);
2882
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, false, note);
3038
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note });
2883
3039
  } catch (error) {
2884
3040
  const msg = error instanceof Error ? error.message + `
2885
3041
  ` + error.stack : String(error);
@@ -2906,7 +3062,7 @@ function smartCompactExtension(pi) {
2906
3062
  try {
2907
3063
  const usage = ctx.getContextUsage();
2908
3064
  const totalTokens = usage?.tokens ?? 0;
2909
- if (!totalTokens || totalTokens < 5000)
3065
+ if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
2910
3066
  return;
2911
3067
  const cur = ctx.model;
2912
3068
  if (!cur)
@@ -2915,7 +3071,7 @@ function smartCompactExtension(pi) {
2915
3071
  if (!sumModel)
2916
3072
  return;
2917
3073
  if (!isRunning.value) {
2918
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, config.profile, false, false, pendingRef, isRunning, true);
3074
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true });
2919
3075
  if (pendingRef.value) {
2920
3076
  const c = pendingRef.value;
2921
3077
  pendingRef.value = null;
@@ -2923,7 +3079,9 @@ function smartCompactExtension(pi) {
2923
3079
  return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
2924
3080
  }
2925
3081
  }
2926
- } catch {}
3082
+ } catch (e) {
3083
+ warn("session_before_compact error", e);
3084
+ }
2927
3085
  });
2928
3086
  pi.registerTool({
2929
3087
  name: "smart_compact",
@@ -2952,7 +3110,7 @@ function smartCompactExtension(pi) {
2952
3110
  }
2953
3111
  try {
2954
3112
  const toolStart = Date.now();
2955
- await runSmartCompact(ctx, sumModel, segModel ?? sumModel, resolvedProfile, verbose, dryRun, pendingRef, isRunning, true, undefined, true);
3113
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
2956
3114
  const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
2957
3115
  if (pendingRef.value) {
2958
3116
  return { content: [{ type: "text", text: "Smart summary generated (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? "?") + " -> " + (pendingRef.value.summary?.length ?? 0) + " chars (" + toolSecs + `s).
@@ -2968,12 +3126,6 @@ TTL: ` + Math.round(PENDING_TTL_MS / 60000) + " minutes." }] };
2968
3126
  }
2969
3127
  });
2970
3128
  }
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
3129
  export {
2978
3130
  smartCompactExtension as default
2979
3131
  };