pi-smart-compact 7.16.0 → 7.18.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.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/constants.ts
19
- var VERSION = "7.16.0", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
19
+ var VERSION = "7.18.1", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
20
20
  {PREV_CONTEXT}
21
21
 
22
22
  {EXTRACTION_CONTEXT}
@@ -39,7 +39,7 @@ IMMUTABLE CONTEXT (verified deterministic data):
39
39
  {EXPLORATION_CONTEXT}
40
40
  {PREV_CONTEXT}
41
41
 
42
- <summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, MAX_TOOL_OUTPUT_CHARS = 800, METRICS_BUFFER_MAX = 200, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
42
+ <summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, FIVE_MINUTES_MS, ONE_HOUR_MS, SEVEN_DAYS_MS, THIRTY_DAYS_MS, EXTRACTION_CACHE_PREFIX = "compact-extraction-", ID_PREFIX, TUNING, TRUNC, MAX_TOOL_OUTPUT_CHARS = 800, LIKELY_ERROR_RE, ERROR_SCAN_MAX_LEN = 2000, ERROR_RETRY_WINDOW = 6, ERROR_RESOLVE_WINDOW = 10, METRICS_BUFFER_MAX = 200, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
43
43
  var init_constants = __esm(() => {
44
44
  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.";
45
45
  PROFILES = {
@@ -81,7 +81,7 @@ var init_constants = __esm(() => {
81
81
  pinPaths: []
82
82
  };
83
83
  NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
84
- SHIFT_RE = /simdi|peki|bide|bi de|gecelim|bakalim|yapalim|baska|sonra|tamam simdi|now let|also|next|let's|moving on|switch to/i;
84
+ SHIFT_RE = /\u015Fimdi|simdi|peki|bi de|bide|ge\u00E7elim|gecelim|bakal\u0131m|bakalim|yapal\u0131m|yapalim|ba\u015Fka|baska|sonra|tamam \u015Fimdi|tamam simdi|now let|also|next|let's|moving on|switch to/iu;
85
85
  CHOICE_RE = /use\s+\S+\s+(?:instead|not|rather)|don't\s+use|avoid\s+|switch\s+to\s+|go\s+with\s+|prefer\s+/i;
86
86
  SINGLE_PASS_PREFIX = `Summarize this coding agent conversation. Produce ONE structured summary.
87
87
  ` + `
@@ -178,6 +178,56 @@ Rules for Accuracy:
178
178
  discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
179
179
  };
180
180
  BACKUP_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
181
+ FIVE_MINUTES_MS = 5 * 60 * 1000;
182
+ ONE_HOUR_MS = 60 * 60 * 1000;
183
+ SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
184
+ THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
185
+ ID_PREFIX = {
186
+ PROJECT: "proj-",
187
+ COMPACT_SESSION: "sc-",
188
+ MULTI_TOOL_USE_SYNTHETIC: "mtu_",
189
+ OPEN_LOOP: "loop-",
190
+ DECISION: "decision-",
191
+ ERROR: "error-"
192
+ };
193
+ TUNING = {
194
+ EMA_PREV: 0.7,
195
+ EMA_SAMPLE: 0.3,
196
+ CALIBRATION_CLAMP_MIN: 0.3,
197
+ CALIBRATION_CLAMP_MAX: 3,
198
+ CONFIDENCE_HIGH: 0.85,
199
+ CONFIDENCE_MEDIUM: 0.8,
200
+ CONFIDENCE_LOW: 0.6
201
+ };
202
+ TRUNC = {
203
+ MESSAGE: 300,
204
+ ERROR_DETAIL: 500,
205
+ DECISION_SUMMARY: 200,
206
+ USER_RESPONSE: 300,
207
+ CONSTRAINT_TEXT: 300,
208
+ OPEN_LOOP_SUMMARY: 120,
209
+ TIMELINE_EVENT: 150,
210
+ TIMELINE_ERROR: 100,
211
+ SNIPPET: 80,
212
+ PREVIEW: 150,
213
+ PREVIEW_MID: 200,
214
+ DETAIL: 300,
215
+ PREVIEW_LONG: 400,
216
+ PREVIEW_XL: 500,
217
+ CHUNK_FALLBACK: 180,
218
+ DECISION_DETAIL: 60,
219
+ TOPIC_LABEL: 100,
220
+ PROJ_ID_HASH: 12,
221
+ CONV_HASH: 8,
222
+ RESULT_GAPS: 5,
223
+ SESSION_ID_DISPLAY: 20,
224
+ ERROR_SNIPPET: 30,
225
+ TIMELINE_DISPLAY: 10,
226
+ EXPLORE_RESULTS: 15,
227
+ BACKUP_PREVIEW_LINES: 5,
228
+ FINGERPRINT_SEG: 2
229
+ };
230
+ LIKELY_ERROR_RE = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i;
181
231
  EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
182
232
 
183
233
  ` + `Your job:
@@ -247,7 +297,7 @@ function damageReportsFile() {
247
297
  return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
248
298
  }
249
299
  function extractionCacheFile(sessionId) {
250
- return path.join(cacheDir(), "compact-extraction-" + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
300
+ return path.join(cacheDir(), EXTRACTION_CACHE_PREFIX + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
251
301
  }
252
302
  function projectFingerprintFile(projectId) {
253
303
  return path.join(projectFingerprintDir(), projectId + ".json");
@@ -261,7 +311,9 @@ function remediationHintsFile(projectId) {
261
311
  function metricsDashboardFile() {
262
312
  return path.join(cacheDir(), "smart-compact-report.html");
263
313
  }
264
- var init_paths = () => {};
314
+ var init_paths = __esm(() => {
315
+ init_constants();
316
+ });
265
317
 
266
318
  // src/infra/fs.ts
267
319
  import fs from "fs";
@@ -389,8 +441,8 @@ class TokenCalibrationStore {
389
441
  const key = calibrationKey(provider, model);
390
442
  const prev = this.factors.get(key) ?? 1;
391
443
  const sample = actual / estimated;
392
- const clamped = Math.max(0.3, Math.min(3, sample));
393
- this.factors.set(key, prev * 0.7 + clamped * 0.3);
444
+ const clamped = Math.max(TUNING.CALIBRATION_CLAMP_MIN, Math.min(TUNING.CALIBRATION_CLAMP_MAX, sample));
445
+ this.factors.set(key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE);
394
446
  }
395
447
  }
396
448
  function calibrationKey(provider, model) {
@@ -398,7 +450,21 @@ function calibrationKey(provider, model) {
398
450
  }
399
451
  function estimateTokens(text, provider, model, calibration = _fallbackCalibration) {
400
452
  const baseRatio = provider ? getProviderCaps(provider).tokenRatioEstimate : CHARS_PER_TOKEN;
401
- const jsonPenalty = text.startsWith("[") || text.startsWith("{") ? 0.85 : 1;
453
+ const startsJson = text.startsWith("[") || text.startsWith("{");
454
+ let jsonPenalty = 1;
455
+ if (startsJson) {
456
+ jsonPenalty = JSON_PENALTY;
457
+ } else if (text.length > 0) {
458
+ const sample = text.length > JSON_DENSITY_SCAN_CAP ? text.slice(0, JSON_DENSITY_SCAN_CAP) : text;
459
+ let structural = 0;
460
+ for (let i = 0;i < sample.length; i++) {
461
+ const c = sample.charCodeAt(i);
462
+ if (c === 34 || c === 91 || c === 93 || c === 123 || c === 125)
463
+ structural++;
464
+ }
465
+ if (structural / sample.length > JSON_DENSITY_THRESHOLD)
466
+ jsonPenalty = JSON_PENALTY;
467
+ }
402
468
  const langPenalty = /[\u00E7\u011F\u0131\u00F6\u015F\u00FC\u00C7\u011E\u0130\u00D6\u015E\u00DC]/.test(text) ? 0.9 : 1;
403
469
  const factor = calibration.get(provider, model);
404
470
  return Math.ceil(text.length / baseRatio * jsonPenalty * langPenalty * factor);
@@ -406,7 +472,7 @@ function estimateTokens(text, provider, model, calibration = _fallbackCalibratio
406
472
  function calibrateFromResponse(estimated, actual, provider, model, calibration = _fallbackCalibration) {
407
473
  calibration.calibrate(estimated, actual, provider, model);
408
474
  }
409
- var PROVIDER_MAP, PROVIDER_ALIASES, DEFAULT_CAPS, _fallbackCalibration;
475
+ var PROVIDER_MAP, PROVIDER_ALIASES, DEFAULT_CAPS, _fallbackCalibration, JSON_PENALTY = 0.85, JSON_DENSITY_THRESHOLD = 0.05, JSON_DENSITY_SCAN_CAP = 8192;
410
476
  var init_tokens = __esm(() => {
411
477
  init_constants();
412
478
  PROVIDER_MAP = {
@@ -671,7 +737,7 @@ var init_llm_retry = __esm(() => {
671
737
  });
672
738
 
673
739
  // src/infra/llm-client.ts
674
- import { complete } from "@earendil-works/pi-ai";
740
+ import { complete } from "@earendil-works/pi-ai/compat";
675
741
  function getLlmClient() {
676
742
  return _client;
677
743
  }
@@ -744,7 +810,7 @@ import crypto4 from "crypto";
744
810
  class ToolSupportCache {
745
811
  entries = new Map;
746
812
  ttlMs;
747
- constructor(ttlMs = 60 * 60 * 1000) {
813
+ constructor(ttlMs = ONE_HOUR_MS) {
748
814
  this.ttlMs = ttlMs;
749
815
  }
750
816
  get(key, now) {
@@ -850,11 +916,7 @@ var init_services = __esm(() => {
850
916
 
851
917
  // src/utils/cache.ts
852
918
  import fs3 from "fs";
853
- import path4 from "path";
854
- import crypto5 from "crypto";
855
- function fallbackSessionId() {
856
- return "sc-" + Date.now().toString(36) + "-" + crypto5.randomBytes(4).toString("hex");
857
- }
919
+ import path5 from "path";
858
920
  function cacheOpts(opts, provider, phase, services) {
859
921
  if (phase && INTERNAL_PHASES.has(phase)) {
860
922
  return { ...opts, cacheRetention: "none" };
@@ -864,7 +926,7 @@ function cacheOpts(opts, provider, phase, services) {
864
926
  if (retention === "none") {
865
927
  return { ...opts, cacheRetention: "none" };
866
928
  }
867
- return { ...opts, sessionId: services?.compactSessionId ?? fallbackSessionId(), cacheRetention: retention };
929
+ return { ...opts, sessionId: services?.compactSessionId ?? makeCompactSessionId(), cacheRetention: retention };
868
930
  }
869
931
  function recordMetric(m, services = getDefaultServices()) {
870
932
  services.metrics.record(m);
@@ -970,14 +1032,14 @@ function scheduleExtractionCacheCleanup() {
970
1032
  _extractionPruneInFlight = true;
971
1033
  queueMicrotask(() => {
972
1034
  try {
973
- const dir = path4.dirname(getCachePath("_"));
1035
+ const dir = path5.dirname(getCachePath("_"));
974
1036
  if (!fs3.existsSync(dir))
975
1037
  return;
976
1038
  const now = Date.now();
977
1039
  for (const name of fs3.readdirSync(dir)) {
978
- if (!name.startsWith("compact-extraction-") || !name.endsWith(".json"))
1040
+ if (!name.startsWith(EXTRACTION_CACHE_PREFIX) || !name.endsWith(".json"))
979
1041
  continue;
980
- const fp = path4.join(dir, name);
1042
+ const fp = path5.join(dir, name);
981
1043
  try {
982
1044
  const stat = fs3.statSync(fp);
983
1045
  if (now - stat.mtimeMs > EXTRACTION_CACHE_PRUNE_MAX_AGE_MS) {
@@ -1023,9 +1085,9 @@ function mergeExtractions(base, delta, baseMsgCount) {
1023
1085
  constraints: [...base.constraints, ...offsetConstraints],
1024
1086
  topics: [...base.topics, ...offsetTopics],
1025
1087
  timeline: [...base.timeline, ...offsetTimeline],
1026
- mainGoal: delta.mainGoal ?? base.mainGoal,
1027
- lastUserMessages: delta.lastUserMessages.length > 0 ? delta.lastUserMessages : base.lastUserMessages,
1028
- lastErrors: delta.lastErrors.length > 0 ? delta.lastErrors : base.lastErrors,
1088
+ mainGoal: base.mainGoal ?? delta.mainGoal,
1089
+ lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
1090
+ lastErrors: [...base.lastErrors, ...delta.lastErrors].slice(-3),
1029
1091
  messageCount: baseMsgCount + delta.messageCount
1030
1092
  };
1031
1093
  }
@@ -1321,6 +1383,7 @@ var init_cache = __esm(() => {
1321
1383
  init_llm_client();
1322
1384
  init_paths();
1323
1385
  init_fs();
1386
+ init_constants();
1324
1387
  init_id_fingerprint();
1325
1388
  init_services();
1326
1389
  INTERNAL_PHASES = new Set([
@@ -1333,8 +1396,8 @@ var init_cache = __esm(() => {
1333
1396
  "assemble",
1334
1397
  "patch"
1335
1398
  ]);
1336
- EXTRACTION_CACHE_TTL_MS = 60 * 60 * 1000;
1337
- EXTRACTION_CACHE_PRUNE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
1399
+ EXTRACTION_CACHE_TTL_MS = ONE_HOUR_MS;
1400
+ EXTRACTION_CACHE_PRUNE_MAX_AGE_MS = SEVEN_DAYS_MS;
1338
1401
  });
1339
1402
 
1340
1403
  // src/ui/dashboard-format.ts
@@ -1451,7 +1514,7 @@ __export(exports_overlays, {
1451
1514
  });
1452
1515
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
1453
1516
  import { Container, Key, matchesKey, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
1454
- import path5 from "path";
1517
+ import path6 from "path";
1455
1518
  function renderContextBar(theme, pct, tokens, barLen = 24) {
1456
1519
  const clamped = Math.min(Math.max(pct, 0), 100);
1457
1520
  const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
@@ -1634,7 +1697,7 @@ async function showResultScreen(ctx, details, extraction, services) {
1634
1697
  const f = modFiles[i];
1635
1698
  const fc = extraction.modifiedFiles.find((e) => e.path === f);
1636
1699
  const count = fc ? " (" + fc.toolCalls + "x)" : "";
1637
- c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path5.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
1700
+ c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path6.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
1638
1701
  }
1639
1702
  if (modFiles.length > maxShow) {
1640
1703
  c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
@@ -1657,7 +1720,7 @@ async function showResultScreen(ctx, details, extraction, services) {
1657
1720
  c.addChild(new Text(theme.fg("success", " \u2705 All facts verified \u2014 no gaps detected"), 0, 0));
1658
1721
  } else if (details.gaps.length > 0) {
1659
1722
  c.addChild(new Text(theme.fg("warning", " \u26A0\uFE0F " + details.gaps.length + " gap(s) patched:"), 0, 0));
1660
- for (const g of details.gaps.slice(0, 5)) {
1723
+ for (const g of details.gaps.slice(0, TRUNC.RESULT_GAPS)) {
1661
1724
  c.addChild(new Text(theme.fg("dim", " \u2022 " + g), 0, 0));
1662
1725
  }
1663
1726
  }
@@ -1808,7 +1871,7 @@ async function showRestorePicker(ctx, backups) {
1808
1871
  const items = backups.map((b) => ({
1809
1872
  value: b.path,
1810
1873
  label: new Date(b.date).toLocaleString() + " \xB7 " + Math.max(1, Math.round(b.sizeBytes / 1024)) + "KB",
1811
- description: b.sessionId.slice(0, 20)
1874
+ description: b.sessionId.slice(0, TRUNC.SESSION_ID_DISPLAY)
1812
1875
  }));
1813
1876
  return await ctx.ui.custom((tui, theme, _kb, done) => {
1814
1877
  const c = new Container;
@@ -1920,6 +1983,7 @@ async function showRestoreAction(ctx, backupPath) {
1920
1983
  });
1921
1984
  }
1922
1985
  var init_overlays = __esm(() => {
1986
+ init_constants();
1923
1987
  init_cache();
1924
1988
  init_tokens();
1925
1989
  });
@@ -1932,1151 +1996,1161 @@ init_constants();
1932
1996
  init_logger();
1933
1997
  init_paths();
1934
1998
  init_fs();
1935
- init_constants();
1936
1999
  import fs2 from "fs";
1937
- import path3 from "path";
2000
+ import path4 from "path";
1938
2001
  import crypto2 from "crypto";
1939
- var VALID_PROFILES = ["light", "balanced", "aggressive"];
1940
- var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
1941
- function validateSmartCompactConfig(sc) {
1942
- if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
1943
- warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
1944
- delete sc.profile;
1945
- }
1946
- if ("autoTrigger" in sc && typeof sc.autoTrigger !== "boolean") {
1947
- warn("smart-compact config: autoTrigger must be boolean, got " + typeof sc.autoTrigger);
1948
- delete sc.autoTrigger;
1949
- }
1950
- if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
1951
- warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
1952
- delete sc.backupEnabled;
1953
- }
1954
- if ("summaryModel" in sc && sc.summaryModel !== null && typeof sc.summaryModel !== "string") {
1955
- warn("smart-compact config: summaryModel must be string|null, got " + typeof sc.summaryModel);
1956
- delete sc.summaryModel;
1957
- }
1958
- if ("segmentationModel" in sc && sc.segmentationModel !== null && typeof sc.segmentationModel !== "string") {
1959
- warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
1960
- delete sc.segmentationModel;
1961
- }
1962
- if ("profiles" in sc) {
1963
- if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
1964
- warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
1965
- delete sc.profiles;
1966
- } else {
1967
- const profiles = sc.profiles;
1968
- for (const [profileName, value] of Object.entries(profiles)) {
1969
- if (!VALID_PROFILES.includes(profileName)) {
1970
- warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
1971
- delete profiles[profileName];
1972
- continue;
1973
- }
1974
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1975
- warn("smart-compact config: profile '" + profileName + "' must be an object.");
1976
- delete profiles[profileName];
1977
- continue;
1978
- }
1979
- const profileCfg = value;
1980
- for (const [key, raw] of Object.entries(profileCfg)) {
1981
- if (!PROFILE_NUMERIC_KEYS.includes(key)) {
1982
- warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
1983
- delete profileCfg[key];
1984
- continue;
1985
- }
1986
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
1987
- warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
1988
- delete profileCfg[key];
1989
- }
1990
- }
1991
- }
1992
- }
1993
- }
1994
- if ("autoTriggerTimeoutMs" in sc) {
1995
- const v = sc.autoTriggerTimeoutMs;
1996
- if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
1997
- warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
1998
- delete sc.autoTriggerTimeoutMs;
1999
- }
2000
- }
2001
- if ("minContextPercent" in sc) {
2002
- const v = sc.minContextPercent;
2003
- if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
2004
- warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
2005
- delete sc.minContextPercent;
2006
- }
2007
- }
2008
- if ("backupDir" in sc && sc.backupDir !== undefined && typeof sc.backupDir !== "string") {
2009
- warn("smart-compact config: backupDir must be a string, got " + typeof sc.backupDir + ". Using default.");
2010
- delete sc.backupDir;
2011
- }
2012
- if ("pinPaths" in sc && sc.pinPaths !== undefined) {
2013
- if (!Array.isArray(sc.pinPaths) || !sc.pinPaths.every((x) => typeof x === "string")) {
2014
- warn("smart-compact config: pinPaths must be a string[], ignoring.");
2015
- delete sc.pinPaths;
2016
- }
2017
- }
2018
- }
2019
- var _cfg = null;
2020
- var _cfgMtime = 0;
2021
- var _cfgPath = null;
2022
- function loadConfig() {
2023
- try {
2024
- const p = settingsFile();
2025
- const stat = fs2.statSync(p);
2026
- if (_cfg && _cfgPath === p && stat.mtimeMs === _cfgMtime)
2027
- return _cfg;
2028
- const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
2029
- const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
2030
- validateSmartCompactConfig(sc);
2031
- const merged = { ...DEFAULT_CONFIG, ...sc };
2032
- if (sc.profiles)
2033
- merged.profiles = { ...PROFILES, ...sc.profiles };
2034
- if (!merged.backupDir)
2035
- merged.backupDir = defaultBackupDir();
2036
- _cfg = merged;
2037
- _cfgMtime = stat.mtimeMs;
2038
- _cfgPath = p;
2039
- return _cfg;
2040
- } catch (e) {
2041
- debug("loadConfig: settings.json not found or unreadable, using defaults", e);
2042
- const fallback = { ...DEFAULT_CONFIG, backupDir: defaultBackupDir() };
2043
- _cfg = fallback;
2044
- _cfgPath = null;
2045
- return fallback;
2046
- }
2047
- }
2048
- var _pruneInFlight = new Set;
2049
- function prunePass(dir) {
2050
- try {
2051
- const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => {
2052
- const full = path3.join(dir, name);
2053
- try {
2054
- return { full, mtimeMs: fs2.statSync(full).mtimeMs };
2055
- } catch {
2056
- return null;
2057
- }
2058
- }).filter((v) => v !== null);
2059
- const now = Date.now();
2060
- const overAge = entries.filter((e) => now - e.mtimeMs > BACKUP_MAX_AGE_MS);
2061
- const sorted = entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
2062
- const overCount = sorted.slice(BACKUP_MAX_FILES);
2063
- const toRemove = new Set([...overAge, ...overCount].map((e) => e.full));
2064
- for (const full of toRemove) {
2065
- try {
2066
- fs2.unlinkSync(full);
2067
- } catch (e) {
2068
- debug("prunePass unlink failed", e);
2069
- }
2070
- }
2071
- } catch (e) {
2072
- debug("prunePass scan failed", e);
2073
- }
2002
+
2003
+ // src/utils/extraction.ts
2004
+ init_constants();
2005
+ init_tokens();
2006
+ import path3 from "path";
2007
+
2008
+ // src/utils/type-guards.ts
2009
+ function isTextBlock(c) {
2010
+ return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
2074
2011
  }
2075
- function schedulePruneBackups(dir) {
2076
- if (_pruneInFlight.has(dir))
2077
- return;
2078
- _pruneInFlight.add(dir);
2079
- queueMicrotask(() => {
2080
- try {
2081
- prunePass(dir);
2082
- } finally {
2083
- _pruneInFlight.delete(dir);
2084
- }
2085
- });
2012
+ function isToolCallBlock(c) {
2013
+ return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
2086
2014
  }
2087
- function backupConversation(convText, sessionId) {
2088
- try {
2089
- const cfg = loadConfig();
2090
- if (!cfg.backupEnabled)
2091
- return null;
2092
- const dir = cfg.backupDir;
2093
- ensureDir(dir);
2094
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
2095
- const hash = crypto2.createHash("sha256").update(convText).digest("hex").slice(0, 8);
2096
- const fp = path3.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
2097
- atomicWriteFileSync(fp, `# Smart Compact Backup
2098
- # Date: ` + new Date().toISOString() + `
2099
- # Session: ` + sessionId + `
2100
-
2101
- ` + convText);
2102
- schedulePruneBackups(dir);
2103
- return fp;
2104
- } catch (e) {
2105
- warn("backupConversation failed", e);
2106
- return null;
2107
- }
2015
+ function getToolCallNames(content) {
2016
+ if (!Array.isArray(content))
2017
+ return [];
2018
+ return content.filter(isToolCallBlock).map((b) => b.name);
2108
2019
  }
2109
- function listBackups(limit = 20) {
2110
- try {
2111
- const dir = loadConfig().backupDir;
2112
- if (!fs2.existsSync(dir))
2113
- return [];
2114
- const out = [];
2115
- for (const name of fs2.readdirSync(dir)) {
2116
- if (!name.endsWith(".md"))
2117
- continue;
2118
- const full = path3.join(dir, name);
2119
- try {
2120
- const stat = fs2.statSync(full);
2121
- if (!stat.isFile())
2122
- continue;
2123
- const head = fs2.readFileSync(full, "utf-8").split(`
2124
- `).slice(0, 5).join(`
2125
- `);
2126
- const date = head.match(/^# Date:\s*(.+)$/m)?.[1]?.trim();
2127
- const session = head.match(/^# Session:\s*(.+)$/m)?.[1]?.trim();
2128
- out.push({
2129
- path: full,
2130
- sessionId: session ?? name,
2131
- date: date ?? stat.mtime.toISOString(),
2132
- sizeBytes: stat.size
2133
- });
2134
- } catch {}
2135
- }
2136
- out.sort((a, b) => a.date < b.date ? 1 : a.date > b.date ? -1 : 0);
2137
- return out.slice(0, limit);
2138
- } catch (e) {
2139
- warn("listBackups failed", e);
2020
+ function filterToolCalls(content) {
2021
+ if (!Array.isArray(content))
2140
2022
  return [];
2141
- }
2023
+ return content.filter(isToolCallBlock);
2142
2024
  }
2143
- function readBackupContent(fp) {
2144
- try {
2145
- const raw = fs2.readFileSync(fp, "utf-8");
2146
- const lines = raw.split(`
2147
- `);
2148
- let i = 0;
2149
- while (i < lines.length && lines[i].startsWith("#"))
2150
- i++;
2151
- if (i < lines.length && lines[i].trim() === "")
2152
- i++;
2153
- return lines.slice(i).join(`
2154
- `).trim() || null;
2155
- } catch (e) {
2156
- warn("readBackupContent failed", e);
2157
- return null;
2158
- }
2025
+ var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
2026
+ var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
2027
+ function isStringArray(v) {
2028
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
2159
2029
  }
2160
- function buildRestoreMessage(content, source) {
2161
- return {
2162
- customType: "smart-compact-restore",
2163
- content: `# Restored pre-compaction context (smart-compact backup)
2164
- Source: ` + source + `
2165
-
2166
- ` + content,
2167
- display: true,
2168
- details: { source, restoredAt: Date.now() }
2169
- };
2030
+ function isValidSmartCompactDetails(d) {
2031
+ if (!d || typeof d !== "object")
2032
+ return false;
2033
+ const r = d;
2034
+ if (!isStringArray(r.modifiedFiles))
2035
+ return false;
2036
+ if (!isStringArray(r.readFiles))
2037
+ return false;
2038
+ if (!isStringArray(r.topics))
2039
+ return false;
2040
+ if (typeof r.method !== "string" || !KNOWN_METHODS.has(r.method))
2041
+ return false;
2042
+ if (typeof r.profile !== "string" || !KNOWN_PROFILES.has(r.profile))
2043
+ return false;
2044
+ if (typeof r.qualityScore !== "number" || !Number.isFinite(r.qualityScore))
2045
+ return false;
2046
+ if (typeof r.totalMessages !== "number" || !Number.isFinite(r.totalMessages))
2047
+ return false;
2048
+ if (r.gaps !== undefined && !isStringArray(r.gaps))
2049
+ return false;
2050
+ if (r.verified !== undefined && typeof r.verified !== "boolean")
2051
+ return false;
2052
+ if (r.backupPath !== undefined && r.backupPath !== null && typeof r.backupPath !== "string")
2053
+ return false;
2054
+ return true;
2170
2055
  }
2171
- function getPreviousCompactionContext(branch) {
2172
- const compactions = branch.filter((e) => e.type === "compaction");
2173
- if (!compactions.length)
2174
- return "";
2175
- const last = compactions[compactions.length - 1];
2176
- const topics = last.details?.topics ?? [];
2177
- if (!topics.length)
2178
- return "";
2179
- return `
2180
- [IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
2056
+ function sanitizeSmartCompactDetails(d) {
2057
+ if (isValidSmartCompactDetails(d))
2058
+ return d;
2059
+ if (!d || typeof d !== "object")
2060
+ return null;
2061
+ const r = d;
2062
+ if (!isStringArray(r.modifiedFiles) || !isStringArray(r.readFiles) || !isStringArray(r.topics))
2063
+ return null;
2064
+ const repaired = {
2065
+ method: KNOWN_METHODS.has(r.method) ? r.method : "heuristic",
2066
+ chunkCount: typeof r.chunkCount === "number" ? r.chunkCount : 0,
2067
+ topics: r.topics,
2068
+ readFiles: r.readFiles,
2069
+ modifiedFiles: r.modifiedFiles,
2070
+ totalMessages: typeof r.totalMessages === "number" ? r.totalMessages : 0,
2071
+ totalTokensSummarized: typeof r.totalTokensSummarized === "number" ? r.totalTokensSummarized : 0,
2072
+ llmCalls: typeof r.llmCalls === "number" ? r.llmCalls : 0,
2073
+ profile: KNOWN_PROFILES.has(r.profile) ? r.profile : "balanced",
2074
+ backupPath: typeof r.backupPath === "string" ? r.backupPath : null,
2075
+ tokensSaved: typeof r.tokensSaved === "number" ? r.tokensSaved : 0,
2076
+ verified: typeof r.verified === "boolean" ? r.verified : false,
2077
+ gaps: isStringArray(r.gaps) ? r.gaps : [],
2078
+ explorationRounds: typeof r.explorationRounds === "number" ? r.explorationRounds : 0,
2079
+ explorationBoundaries: typeof r.explorationBoundaries === "number" ? r.explorationBoundaries : 0,
2080
+ model: typeof r.model === "string" ? r.model : "unknown",
2081
+ qualityScore: typeof r.qualityScore === "number" ? r.qualityScore : 0,
2082
+ tokensBefore: typeof r.tokensBefore === "number" ? r.tokensBefore : 0
2083
+ };
2084
+ return isValidSmartCompactDetails(repaired) ? repaired : null;
2181
2085
  }
2182
- function findLastAnchorIndex(branchEntries) {
2183
- for (let i = branchEntries.length - 1;i >= 0; i--) {
2184
- const e = branchEntries[i];
2185
- if (e?.type !== "message")
2186
- continue;
2187
- const msg = e.message;
2188
- if (msg?.role !== "toolResult")
2189
- continue;
2190
- if (msg?.toolName === "context" && msg?.details?.anchor) {
2191
- return i;
2192
- }
2086
+
2087
+ // src/utils/file-needles.ts
2088
+ var GENERIC_BASENAMES = new Set([
2089
+ "index.ts",
2090
+ "index.js",
2091
+ "index.tsx",
2092
+ "index.jsx",
2093
+ "types.ts",
2094
+ "helpers.ts",
2095
+ "utils.ts",
2096
+ "main.ts",
2097
+ "main.js",
2098
+ "mod.rs",
2099
+ "lib.rs",
2100
+ "__init__.py"
2101
+ ]);
2102
+ var MIN_BARE_BASENAME_LEN = 5;
2103
+ function buildPathNeedles(filePath) {
2104
+ const parts = filePath.toLowerCase().split("/").filter(Boolean);
2105
+ if (parts.length === 0)
2106
+ return [];
2107
+ const needles = [];
2108
+ const basename = parts[parts.length - 1];
2109
+ if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
2110
+ needles.push(basename);
2193
2111
  }
2194
- return -1;
2112
+ for (let j = parts.length - 2;j >= 0; j--) {
2113
+ needles.push(parts.slice(j).join("/"));
2114
+ }
2115
+ return needles;
2195
2116
  }
2196
- function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
2197
- let msgCount = 0;
2198
- for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
2199
- const e = branchEntries[i];
2200
- if (e?.type === "message") {
2201
- if (msgCount >= msgs.length)
2202
- return msgs.length - 1;
2203
- msgCount++;
2204
- }
2117
+
2118
+ // src/domain/tool-semantics.ts
2119
+ var PATH_KEYS = ["path", "file_path", "filePath", "filename", "file"];
2120
+ var PAYLOAD_KEYS = ["content", "newText", "oldText", "edits", "patch", "replacement"];
2121
+ var COMMAND_KEYS = ["command", "cmd", "script"];
2122
+ function hasPresent(args, keys) {
2123
+ return keys.some((k) => args[k] != null);
2124
+ }
2125
+ function extractToolPath(args) {
2126
+ if (!args || typeof args !== "object")
2127
+ return;
2128
+ const a = args;
2129
+ for (const k of PATH_KEYS) {
2130
+ const v = a[k];
2131
+ if (typeof v === "string" && v.length > 0)
2132
+ return v;
2133
+ }
2134
+ return;
2135
+ }
2136
+ function classifyTool(args) {
2137
+ if (!args || typeof args !== "object")
2138
+ return "other";
2139
+ const a = args;
2140
+ const hasPath = extractToolPath(a) !== undefined;
2141
+ const hasPayload = hasPresent(a, PAYLOAD_KEYS);
2142
+ const hasCommand = hasPresent(a, COMMAND_KEYS);
2143
+ if (hasPath && hasPayload)
2144
+ return "mutates";
2145
+ if (hasCommand)
2146
+ return "executes";
2147
+ if (hasPath)
2148
+ return "accesses";
2149
+ return "other";
2150
+ }
2151
+
2152
+ // src/utils/extraction.ts
2153
+ var TRUNCATE_RE = /\u2026\u2702\d+$/;
2154
+ function isTruncated(content) {
2155
+ return TRUNCATE_RE.test(extractText(content));
2156
+ }
2157
+ var DELETE_RESULT_RE = /\b(?:deleted|removed|unlinked)\b/i;
2158
+ function flattenToolCallBlock(b) {
2159
+ if (!isToolCallBlock(b))
2160
+ return [];
2161
+ if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2162
+ return b.arguments.tool_uses.map((u) => {
2163
+ const recipient = u?.recipient_name ?? "";
2164
+ return {
2165
+ name: recipient.replace(/^functions\./, ""),
2166
+ id: u?.id ?? undefined,
2167
+ arguments: u?.parameters ?? {}
2168
+ };
2169
+ });
2205
2170
  }
2206
- return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
2171
+ return [{ name: b.name, id: b.id, arguments: b.arguments }];
2207
2172
  }
2208
- function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
2209
- let adjusted = keepFromIndex;
2210
- if (branchEntries && branchEntries.length > 0) {
2211
- const lastAnchorBranchIdx = findLastAnchorIndex(branchEntries);
2212
- if (lastAnchorBranchIdx >= 0) {
2213
- const lastAnchorMsgIdx = branchIndexToMsgIndex(branchEntries, lastAnchorBranchIdx, msgs);
2214
- if (adjusted > lastAnchorMsgIdx && lastAnchorMsgIdx >= 0) {
2215
- adjusted = lastAnchorMsgIdx;
2173
+ function extractText(content) {
2174
+ if (typeof content === "string")
2175
+ return content;
2176
+ if (!Array.isArray(content))
2177
+ return "";
2178
+ return content.map((b) => {
2179
+ if (typeof b === "string")
2180
+ return b;
2181
+ if (isTextBlock(b))
2182
+ return b.text;
2183
+ return "";
2184
+ }).join("");
2185
+ }
2186
+ function mediaKind(type, mime) {
2187
+ const s = (type + " " + (mime ?? "")).toLowerCase();
2188
+ if (/image|input_image|image_url/.test(s))
2189
+ return "image";
2190
+ if (/audio/.test(s))
2191
+ return "audio";
2192
+ if (/video/.test(s))
2193
+ return "video";
2194
+ if (/file|document|pdf|attachment/.test(s))
2195
+ return "file";
2196
+ return "unknown";
2197
+ }
2198
+ function extractMediaAttachments(msgs) {
2199
+ const out = [];
2200
+ for (let i = 0;i < msgs.length; i++) {
2201
+ const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
2202
+ for (const b of blocks) {
2203
+ if (!b || typeof b !== "object")
2204
+ continue;
2205
+ const rec = b;
2206
+ const type = String(rec.type ?? "");
2207
+ if (type === "text" || type === "toolCall" || type === "tool_use")
2208
+ continue;
2209
+ const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
2210
+ const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
2211
+ const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
2212
+ const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
2213
+ const kind = mediaKind(type, mimeType);
2214
+ if (kind !== "unknown" || source || mimeType || name) {
2215
+ out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
2216
2216
  }
2217
2217
  }
2218
2218
  }
2219
- if (adjusted <= 0 || adjusted >= msgs.length)
2220
- return adjusted;
2221
- const last = msgs[adjusted - 1];
2222
- const first = msgs[adjusted];
2223
- if (last && first) {
2224
- const getText = (msg) => {
2225
- const m = msg;
2226
- const c = m?.content;
2227
- if (typeof c === "string")
2228
- return c;
2229
- if (Array.isArray(c))
2230
- return c.map((b) => {
2231
- if (typeof b === "string")
2232
- return b;
2233
- if (typeof b === "object" && b !== null && b.type === "text")
2234
- return b.text ?? "";
2235
- return "";
2236
- }).join("");
2237
- return "";
2238
- };
2239
- const lastText = getText(last.message).toLowerCase();
2240
- const keptText = getText(first.message).toLowerCase();
2241
- const fileRe = /(?:path|file)=["']([^"']+)["']/g;
2242
- const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
2243
- fileRe.lastIndex = 0;
2244
- const keptFiles = new Set([...keptText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
2245
- if ([...lastFiles].filter((f) => keptFiles.has(f)).length > 0)
2246
- return adjusted - 1;
2247
- }
2248
- return adjusted;
2219
+ return out;
2249
2220
  }
2250
- function collectToolCallIds(blocks, msgIndex, out) {
2251
- for (const b of blocks) {
2252
- const block = b;
2253
- if (block?.type === "toolCall") {
2254
- if (typeof block.id === "string") {
2255
- out.set(block.id, msgIndex);
2221
+ function buildToolCallIndex(msgs) {
2222
+ const idx = new Map;
2223
+ for (let i = 0;i < msgs.length; i++) {
2224
+ const m = msgs[i];
2225
+ if (m.role !== "assistant")
2226
+ continue;
2227
+ const blocks = Array.isArray(m.content) ? m.content : [];
2228
+ for (const b of blocks) {
2229
+ if (!isToolCallBlock(b))
2230
+ continue;
2231
+ if (b.id) {
2232
+ idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
2256
2233
  }
2257
- const args = block.arguments;
2258
- if (block.name === "multi_tool_use.parallel" && args && Array.isArray(args.tool_uses)) {
2259
- for (const nested of args.tool_uses) {
2260
- const n = nested;
2261
- if (typeof n.id === "string") {
2262
- out.set(n.id, msgIndex);
2263
- }
2234
+ if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2235
+ const nested = flattenToolCallBlock(b);
2236
+ for (let t = 0;t < nested.length; t++) {
2237
+ const tool = nested[t];
2238
+ const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
2239
+ idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
2264
2240
  }
2265
2241
  }
2266
2242
  }
2267
2243
  }
2244
+ return idx;
2268
2245
  }
2269
- function guardToolCallBoundary(msgs, keepFrom) {
2270
- if (keepFrom <= 0 || keepFrom >= msgs.length)
2271
- return keepFrom;
2272
- const tcMap = new Map;
2246
+ function trackFileOps(msgs, _tcIdx) {
2247
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2248
+ const modMap = new Map;
2249
+ const readSet = new Set;
2250
+ const delSet = new Set;
2273
2251
  for (let i = 0;i < msgs.length; i++) {
2274
- const m = msgs[i].message;
2275
- if (m?.role !== "assistant")
2252
+ const m = msgs[i];
2253
+ if (m.role !== "toolResult" || m.isError)
2276
2254
  continue;
2277
- const blocks = Array.isArray(m?.content) ? m.content : [];
2278
- collectToolCallIds(blocks, i, tcMap);
2255
+ const tc = tcIdx.get(m.toolCallId ?? "");
2256
+ if (!tc)
2257
+ continue;
2258
+ const filePath = extractToolPath(tc.arguments);
2259
+ if (!filePath)
2260
+ continue;
2261
+ const cls = classifyTool(tc.arguments);
2262
+ if (cls === "mutates") {
2263
+ const resultText = extractText(m.content);
2264
+ if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
2265
+ const existing = modMap.get(filePath);
2266
+ modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
2267
+ }
2268
+ } else if (cls === "accesses") {
2269
+ if (DELETE_RESULT_RE.test(extractText(m.content)))
2270
+ delSet.add(filePath);
2271
+ else
2272
+ readSet.add(filePath);
2273
+ }
2279
2274
  }
2280
- let adjusted = keepFrom;
2281
- let changed = true;
2282
- const MAX_ITER = msgs.length + 1;
2283
- let iter = 0;
2284
- while (changed) {
2285
- if (++iter > MAX_ITER) {
2286
- warn("guardToolCallBoundary hit MAX_ITER=" + MAX_ITER + " at adjusted=" + adjusted);
2287
- break;
2275
+ return {
2276
+ modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
2277
+ read: [...readSet],
2278
+ deleted: [...delSet]
2279
+ };
2280
+ }
2281
+ function catalogErrors(msgs, _tcIdx) {
2282
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2283
+ const errors = [];
2284
+ for (let i = 0;i < msgs.length; i++) {
2285
+ const m = msgs[i];
2286
+ if (m.role !== "toolResult")
2287
+ continue;
2288
+ const tc = tcIdx.get(m.toolCallId ?? "");
2289
+ if (m.isError) {
2290
+ errors.push({ index: i, tool: tc?.name ?? "unknown", message: extractText(m.content).slice(0, TRUNC.ERROR_DETAIL), retryAttempted: false, resolved: false });
2291
+ continue;
2288
2292
  }
2289
- changed = false;
2290
- for (let i = adjusted;i < msgs.length; i++) {
2291
- const m = msgs[i].message;
2292
- if (m?.role !== "toolResult")
2293
+ if (tc && classifyTool(tc.arguments) === "executes") {
2294
+ const txt = extractText(m.content);
2295
+ if (LIKELY_ERROR_RE.test(txt) && txt.length < ERROR_SCAN_MAX_LEN) {
2296
+ errors.push({ index: i, tool: tc.name, message: txt.slice(0, TRUNC.MESSAGE), retryAttempted: false, resolved: false });
2297
+ }
2298
+ }
2299
+ }
2300
+ for (const err of errors) {
2301
+ for (let j = err.index + 1;j < Math.min(msgs.length, err.index + ERROR_RETRY_WINDOW); j++) {
2302
+ if (msgs[j]?.role !== "assistant")
2293
2303
  continue;
2294
- const tcId = m?.toolCallId;
2295
- if (!tcId)
2304
+ const rawBlocks = msgs[j]?.content;
2305
+ const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
2306
+ const retryTool = blocks.flatMap((b) => flattenToolCallBlock(b)).find((t) => t.name === err.tool);
2307
+ if (!retryTool)
2296
2308
  continue;
2297
- const tcIdx = tcMap.get(tcId);
2298
- if (tcIdx !== undefined && tcIdx < adjusted) {
2299
- adjusted = tcIdx;
2300
- changed = true;
2301
- break;
2309
+ err.retryAttempted = true;
2310
+ for (let k = j + 1;k < Math.min(msgs.length, j + ERROR_RESOLVE_WINDOW); k++) {
2311
+ const mk = msgs[k];
2312
+ if (mk?.role !== "toolResult" || mk.isError)
2313
+ continue;
2314
+ const resolved = retryTool.id != null ? mk.toolCallId === retryTool.id : tcIdx.get(mk.toolCallId ?? "")?.name === err.tool;
2315
+ if (resolved) {
2316
+ err.resolved = true;
2317
+ break;
2318
+ }
2302
2319
  }
2320
+ break;
2303
2321
  }
2304
2322
  }
2305
- return Math.max(0, Math.min(adjusted, msgs.length));
2306
- }
2307
- function extractUserNote(args) {
2308
- const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
2309
- const tokens = args.trim().split(/\s+/).filter(Boolean);
2310
- const isOptionToken = (t) => t.startsWith("--") || SKIP.has(t.toLowerCase()) || /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t);
2311
- const nonFlags = tokens.filter((t) => !isOptionToken(t));
2312
- return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
2323
+ return errors;
2313
2324
  }
2314
- function createBatches(chunks, maxTokens) {
2315
- const batches = [];
2316
- let batch = [], bt = 0;
2317
- for (const ch of chunks) {
2318
- if (batch.length && bt + ch.tokenEstimate > maxTokens) {
2319
- batches.push(batch);
2320
- batch = [];
2321
- bt = 0;
2325
+ function extractDecisions(msgs, _tcIdx) {
2326
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2327
+ const decisions = [];
2328
+ for (const [id, tc] of tcIdx) {
2329
+ if (tc.name !== "ask_user")
2330
+ continue;
2331
+ const args = tc.arguments;
2332
+ const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
2333
+ if (!question)
2334
+ continue;
2335
+ for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
2336
+ if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
2337
+ decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, TRUNC.DECISION_SUMMARY), userResponse: extractText(msgs[i].content).slice(0, TRUNC.USER_RESPONSE) });
2338
+ break;
2339
+ }
2322
2340
  }
2323
- batch.push(ch);
2324
- bt += ch.tokenEstimate;
2325
2341
  }
2326
- if (batch.length)
2327
- batches.push(batch);
2328
- return batches;
2329
- }
2330
- function allocateTopicBudgets(summaries, totalBudget) {
2331
- const n = summaries.length;
2332
- if (n === 0)
2333
- return new Map;
2334
- const weights = summaries.map((s, i) => {
2335
- let w = 1;
2336
- if (s.priority === "critical")
2337
- w *= 2;
2338
- else if (s.priority === "high")
2339
- w *= 1.5;
2340
- else if (s.priority === "low")
2341
- w *= 0.6;
2342
- const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
2343
- w *= 1 + errorKeywords * 0.2;
2344
- const recency = (i + 1) / n;
2345
- w *= 0.6 + recency * 0.4;
2346
- if (s.keyDecisions.length > 0)
2347
- w *= 1.3;
2348
- return w;
2349
- });
2350
- const totalWeight = weights.reduce((a, b) => a + b, 0);
2351
- const baseTokensPerTopic = Math.floor(totalBudget / n);
2352
- const budgetMap = new Map;
2353
- for (let i = 0;i < summaries.length; i++) {
2354
- const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
2355
- budgetMap.set(summaries[i].topic, Math.max(200, allocated));
2342
+ for (let i = 0;i < msgs.length; i++) {
2343
+ if (msgs[i]?.role !== "user")
2344
+ continue;
2345
+ const txt = extractText(msgs[i].content);
2346
+ if (CHOICE_RE.test(txt)) {
2347
+ decisions.push({ index: i, type: "implicit", summary: txt.slice(0, TRUNC.DECISION_SUMMARY) });
2348
+ }
2356
2349
  }
2357
- return budgetMap;
2358
- }
2359
- function preProcessSummaries(summaries, budgetTokens) {
2360
- const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
2361
- return {
2362
- decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
2363
- modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
2364
- read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
2365
- text: summaries.map((cs, i) => {
2366
- const budgetHint = topicBudgets?.get(cs.topic);
2367
- const budgetLine = budgetHint ? `
2368
- Budget: ~` + budgetHint + " tokens" : "";
2369
- return "### Segment " + (i + 1) + ": " + cs.topic + `
2370
- Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
2371
-
2372
- ` + cs.summary + `
2373
-
2374
- Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
2375
- Modified: ` + (cs.filesModified.join(", ") || "None") + `
2376
- Read: ` + (cs.filesRead.join(", ") || "None");
2377
- }).join(`
2378
- ---
2379
- `)
2380
- };
2381
- }
2382
- function buildExtractionContext(extraction, forRange) {
2383
- const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
2384
- const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
2385
- const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
2386
- return [
2387
- "## Deterministic Extraction (verified facts)",
2388
- "Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
2389
- "Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
2390
- "Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 60)).join("; ") || "none"),
2391
- "Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none"),
2392
- "Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
2393
- ].join(`
2394
- `);
2350
+ return decisions;
2395
2351
  }
2396
- function computeToolCharPercentage(branchEntries) {
2397
- let totalChars = 0;
2398
- let toolChars = 0;
2399
- for (const raw of branchEntries) {
2400
- const m = raw?.message;
2401
- if (!m)
2352
+ var CONSTRAINT_PATTERNS = [
2353
+ { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
2354
+ { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
2355
+ { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
2356
+ { re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
2357
+ { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
2358
+ { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
2359
+ ];
2360
+ function mineConstraints(msgs) {
2361
+ const constraints = [];
2362
+ for (let i = 0;i < msgs.length; i++) {
2363
+ if (msgs[i]?.role !== "user")
2402
2364
  continue;
2403
- let mc = 0;
2404
- if (typeof m.content === "string") {
2405
- mc = m.content.length;
2406
- } else if (Array.isArray(m.content)) {
2407
- for (const part of m.content) {
2408
- if (!part || typeof part !== "object")
2409
- continue;
2410
- const block = part;
2411
- if (block.type === "text" && typeof block.text === "string")
2412
- mc += block.text.length;
2365
+ const txt = extractText(msgs[i].content);
2366
+ if (txt.length < 10 || txt.startsWith("/"))
2367
+ continue;
2368
+ for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
2369
+ if (re.test(txt)) {
2370
+ constraints.push({ index: i, text: txt.slice(0, TRUNC.CONSTRAINT_TEXT), category: cat, confidence: conf });
2371
+ break;
2413
2372
  }
2414
2373
  }
2415
- totalChars += mc;
2416
- if (m.role === "toolResult")
2417
- toolChars += mc;
2418
2374
  }
2419
- return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
2375
+ return constraints;
2420
2376
  }
2421
- function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
2422
- if (totalTokens < minThreshold)
2423
- return "none";
2424
- if (contextPercent < minContextPercent)
2425
- return "none";
2426
- if (contextPercent < 80)
2427
- return "light";
2428
- return "full";
2377
+ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2378
+ const topics = [];
2379
+ let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
2380
+ let currentType = "exploration";
2381
+ let currentPrimaryFile = null;
2382
+ const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2383
+ for (let i = 0;i < msgs.length; i++) {
2384
+ const m = msgs[i];
2385
+ const txt = extractText(m.content);
2386
+ tokenAcc += estimateTokens(txt);
2387
+ let brk = false;
2388
+ if (m.role === "assistant") {
2389
+ const blocks = Array.isArray(m.content) ? m.content : [];
2390
+ for (const b of blocks) {
2391
+ for (const tool of flattenToolCallBlock(b)) {
2392
+ const fp = extractToolPath(tool.arguments);
2393
+ if (!fp)
2394
+ continue;
2395
+ const fn = path3.basename(fp);
2396
+ if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
2397
+ brk = true;
2398
+ lastFile = fn;
2399
+ currentPrimaryFile = fp;
2400
+ const toolCls = classifyTool(tool.arguments);
2401
+ if (toolCls === "mutates") {
2402
+ if (currentType !== "implementation")
2403
+ currentType = "implementation";
2404
+ } else if (toolCls === "accesses") {
2405
+ if (currentType === "exploration")
2406
+ currentType = "review";
2407
+ }
2408
+ }
2409
+ }
2410
+ }
2411
+ if (m.role === "toolResult" && m.isError) {
2412
+ errAcc++;
2413
+ if (currentType !== "implementation")
2414
+ currentType = "debugging";
2415
+ }
2416
+ if (m.role === "toolResult" && !m.isError) {
2417
+ const tc = tcIdx.get(m.toolCallId ?? "");
2418
+ if (tc && classifyTool(tc.arguments) === "executes" && /error|fail/i.test(txt)) {
2419
+ errAcc++;
2420
+ if (currentType !== "implementation")
2421
+ currentType = "debugging";
2422
+ }
2423
+ }
2424
+ if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
2425
+ brk = true;
2426
+ if (tokenAcc >= pc.maxChunkTokens)
2427
+ brk = true;
2428
+ if (brk && i > startIdx && topics.length < maxSegs - 1) {
2429
+ topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2430
+ startIdx = i + 1;
2431
+ tokenAcc = 0;
2432
+ lastFile = null;
2433
+ errAcc = 0;
2434
+ currentType = "exploration";
2435
+ currentPrimaryFile = null;
2436
+ }
2437
+ }
2438
+ if (startIdx < msgs.length) {
2439
+ topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2440
+ }
2441
+ return topics;
2429
2442
  }
2430
- function inferSessionType(extraction, report) {
2431
- if (report?.sessionType)
2432
- return report.sessionType;
2433
- const hasModifications = extraction.modifiedFiles.length > 0;
2434
- const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
2435
- const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
2436
- const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
2437
- const hasDecisions = extraction.decisions.length > 0;
2438
- if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
2439
- return "debugging";
2440
- if (hasReadsOnly && !hasDecisions)
2441
- return "review";
2442
- if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
2443
- return "discussion";
2444
- if (hasModifications)
2445
- return "implementation";
2446
- return "implementation";
2447
- }
2448
- function buildExplorationContext(report) {
2449
- if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
2450
- return "";
2451
- return [
2452
- "## Exploration Report",
2453
- "Main goal: " + report.mainGoal,
2454
- "Session type: " + report.sessionType,
2455
- report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
2456
- report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
2457
- report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
2458
- report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
2459
- report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
2460
- ].filter(Boolean).join(`
2461
- `);
2462
- }
2463
-
2464
- // src/index.ts
2465
- init_tokens();
2466
- init_cache();
2467
-
2468
- // src/app/run-smart-compact.ts
2469
- init_overlays();
2470
- init_logger();
2471
-
2472
- // src/app/run-context.ts
2473
- function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
2474
- rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
2475
- rc.phaseStart = endMs;
2476
- }
2477
- function markPhase(rc, phase) {
2478
- markMeasuredPhase(rc, phase, rc.phaseStart);
2443
+ function buildTimeline(msgs, errors) {
2444
+ const timeline = [];
2445
+ const errorIndices = new Set(errors.map((e) => e.index));
2446
+ for (let i = 0;i < msgs.length; i++) {
2447
+ const m = msgs[i];
2448
+ if (m.role === "user") {
2449
+ const txt = extractText(m.content);
2450
+ if (!txt.startsWith("/"))
2451
+ timeline.push({ index: i, event: "user_request", summary: txt.slice(0, TRUNC.TIMELINE_EVENT) });
2452
+ }
2453
+ if (errorIndices.has(i))
2454
+ timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, TRUNC.TIMELINE_ERROR) ?? "error" });
2455
+ }
2456
+ return timeline.length > 30 ? [...timeline.filter((t) => t.event === "user_request").slice(0, TRUNC.TIMELINE_DISPLAY), ...timeline.filter((t) => t.event === "error")] : timeline;
2479
2457
  }
2480
- function advance(rc, _stage) {
2481
- return rc;
2458
+ function extractMainGoal(msgs) {
2459
+ for (const m of msgs) {
2460
+ if (m?.role !== "user")
2461
+ continue;
2462
+ const txt = extractText(m.content).trim();
2463
+ if (txt && !txt.startsWith("/"))
2464
+ return txt.slice(0, TRUNC.MESSAGE);
2465
+ }
2466
+ return null;
2482
2467
  }
2483
-
2484
- // src/app/run-smart-compact.ts
2485
- init_services();
2486
-
2487
- // src/app/steps/prepare.ts
2488
- init_constants();
2489
- init_tokens();
2490
- init_logger();
2491
- async function prepareRun(rc) {
2492
- const config = loadConfig();
2493
- const profileCfg = { ...PROFILES[rc.profile], ...config.profiles?.[rc.profile] ?? {} };
2494
- const providerCaps = getProviderCaps(rc.summaryModel.provider);
2495
- const auth = await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.summaryModel);
2496
- const segAuth = rc.segModel !== rc.summaryModel ? await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.segModel) : auth;
2497
- if (!auth.ok || !auth.apiKey || (!segAuth.ok || !segAuth.apiKey)) {
2498
- if (!rc.flags.autoTriggered)
2499
- rc.ctx.ui.notify("Auth failed", "error");
2500
- return null;
2468
+ function extractOpenLoops(msgs, extraction) {
2469
+ const loops = [];
2470
+ let loopId = 0;
2471
+ const fileNeedles = extraction.modifiedFiles.map((f) => ({
2472
+ path: f.path,
2473
+ needles: buildPathNeedles(f.path)
2474
+ }));
2475
+ for (const err of extraction.errors.filter((e) => !e.resolved)) {
2476
+ const errLower = err.message.toLowerCase();
2477
+ const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path4 }) => path4);
2478
+ loops.push({
2479
+ id: ID_PREFIX.OPEN_LOOP + ++loopId,
2480
+ type: "bugfix",
2481
+ priority: err.retryAttempted ? "high" : "normal",
2482
+ status: "open",
2483
+ summary: err.message.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
2484
+ files: errFiles,
2485
+ sourceIndex: err.index
2486
+ });
2501
2487
  }
2502
- if (rc.timeoutMs > 0) {
2503
- rc.cancellation.timeoutId = setTimeout(() => {
2504
- rc.cancellation.timedOut = true;
2505
- rc.cancellation.controller.abort();
2506
- rc.notify("Smart compact auto-trigger exceeded " + rc.timeoutMs + "ms; Pi will use native compact for this run", "warning");
2507
- }, rc.timeoutMs);
2488
+ const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|yapalim|yapal\u0131m|yapmamiz|yapmam\u0131z|gerekiyor|eklenecek|d\u00FCzeltilecek|duzeltilecek|bitmedi|kaldi|kald\u0131)/iu;
2489
+ for (let idx = 0;idx < msgs.length; idx++) {
2490
+ const msg = msgs[idx];
2491
+ if (msg.role !== "user")
2492
+ continue;
2493
+ const txt = extractText(msg.content);
2494
+ if (txt.length < 10 || txt.startsWith("/"))
2495
+ continue;
2496
+ if (FOLLOWUP_RE.test(txt)) {
2497
+ const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2498
+ if (!isDup) {
2499
+ loops.push({
2500
+ id: ID_PREFIX.OPEN_LOOP + ++loopId,
2501
+ type: "follow-up",
2502
+ priority: "normal",
2503
+ status: "open",
2504
+ summary: txt.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
2505
+ files: [],
2506
+ sourceIndex: idx
2507
+ });
2508
+ }
2509
+ }
2508
2510
  }
2509
- debug("prepareRun: profile=" + rc.profile + " model=" + rc.modelLabel);
2510
- const out = rc;
2511
- out.config = config;
2512
- out.profileCfg = profileCfg;
2513
- out.providerCaps = providerCaps;
2514
- out.summaryAuth = { apiKey: auth.apiKey, headers: auth.headers };
2515
- out.segAuth = { apiKey: segAuth.apiKey, headers: segAuth.headers };
2516
- return advance(out, "_prepared");
2517
- }
2518
-
2519
- // src/app/steps/window.ts
2520
- init_tokens();
2521
-
2522
- // src/utils/extraction.ts
2523
- init_constants();
2524
- init_tokens();
2525
- import path6 from "path";
2526
-
2527
- // src/utils/type-guards.ts
2528
- function isTextBlock(c) {
2529
- return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
2530
- }
2531
- function isToolCallBlock(c) {
2532
- return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
2533
- }
2534
- function getToolCallNames(content) {
2535
- if (!Array.isArray(content))
2536
- return [];
2537
- return content.filter(isToolCallBlock).map((b) => b.name);
2538
- }
2539
- function filterToolCalls(content) {
2540
- if (!Array.isArray(content))
2541
- return [];
2542
- return content.filter(isToolCallBlock);
2543
- }
2544
- var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
2545
- var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
2546
- function isStringArray(v) {
2547
- return Array.isArray(v) && v.every((x) => typeof x === "string");
2548
- }
2549
- function isValidSmartCompactDetails(d) {
2550
- if (!d || typeof d !== "object")
2551
- return false;
2552
- const r = d;
2553
- if (!isStringArray(r.modifiedFiles))
2554
- return false;
2555
- if (!isStringArray(r.readFiles))
2556
- return false;
2557
- if (!isStringArray(r.topics))
2558
- return false;
2559
- if (typeof r.method !== "string" || !KNOWN_METHODS.has(r.method))
2560
- return false;
2561
- if (typeof r.profile !== "string" || !KNOWN_PROFILES.has(r.profile))
2562
- return false;
2563
- if (typeof r.qualityScore !== "number" || !Number.isFinite(r.qualityScore))
2564
- return false;
2565
- if (typeof r.totalMessages !== "number" || !Number.isFinite(r.totalMessages))
2566
- return false;
2567
- if (r.gaps !== undefined && !isStringArray(r.gaps))
2568
- return false;
2569
- if (r.verified !== undefined && typeof r.verified !== "boolean")
2570
- return false;
2571
- if (r.backupPath !== undefined && r.backupPath !== null && typeof r.backupPath !== "string")
2572
- return false;
2573
- return true;
2511
+ const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
2512
+ for (let idx = 0;idx < msgs.length; idx++) {
2513
+ const msg = msgs[idx];
2514
+ if (msg.role !== "user")
2515
+ continue;
2516
+ const txt = extractText(msg.content);
2517
+ if (BLOCKED_RE.test(txt)) {
2518
+ const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2519
+ if (!isDup) {
2520
+ loops.push({
2521
+ id: ID_PREFIX.OPEN_LOOP + ++loopId,
2522
+ type: "blocked",
2523
+ priority: "high",
2524
+ status: "open",
2525
+ summary: txt.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
2526
+ files: [],
2527
+ sourceIndex: idx
2528
+ });
2529
+ }
2530
+ }
2531
+ }
2532
+ for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
2533
+ const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
2534
+ if (!exists) {
2535
+ loops.push({
2536
+ id: ID_PREFIX.OPEN_LOOP + ++loopId,
2537
+ type: "retry",
2538
+ priority: "high",
2539
+ status: "open",
2540
+ summary: "Retried but unresolved: " + err.message.slice(0, TRUNC.SNIPPET),
2541
+ files: [],
2542
+ sourceIndex: err.index
2543
+ });
2544
+ }
2545
+ }
2546
+ return loops;
2574
2547
  }
2575
- function sanitizeSmartCompactDetails(d) {
2576
- if (isValidSmartCompactDetails(d))
2577
- return d;
2578
- if (!d || typeof d !== "object")
2579
- return null;
2580
- const r = d;
2581
- if (!isStringArray(r.modifiedFiles) || !isStringArray(r.readFiles) || !isStringArray(r.topics))
2582
- return null;
2583
- const repaired = {
2584
- method: KNOWN_METHODS.has(r.method) ? r.method : "heuristic",
2585
- chunkCount: typeof r.chunkCount === "number" ? r.chunkCount : 0,
2586
- topics: r.topics,
2587
- readFiles: r.readFiles,
2588
- modifiedFiles: r.modifiedFiles,
2589
- totalMessages: typeof r.totalMessages === "number" ? r.totalMessages : 0,
2590
- totalTokensSummarized: typeof r.totalTokensSummarized === "number" ? r.totalTokensSummarized : 0,
2591
- llmCalls: typeof r.llmCalls === "number" ? r.llmCalls : 0,
2592
- profile: KNOWN_PROFILES.has(r.profile) ? r.profile : "balanced",
2593
- backupPath: typeof r.backupPath === "string" ? r.backupPath : null,
2594
- tokensSaved: typeof r.tokensSaved === "number" ? r.tokensSaved : 0,
2595
- verified: typeof r.verified === "boolean" ? r.verified : false,
2596
- gaps: isStringArray(r.gaps) ? r.gaps : [],
2597
- explorationRounds: typeof r.explorationRounds === "number" ? r.explorationRounds : 0,
2598
- explorationBoundaries: typeof r.explorationBoundaries === "number" ? r.explorationBoundaries : 0,
2599
- model: typeof r.model === "string" ? r.model : "unknown",
2600
- qualityScore: typeof r.qualityScore === "number" ? r.qualityScore : 0,
2601
- tokensBefore: typeof r.tokensBefore === "number" ? r.tokensBefore : 0
2548
+ function extractStructured(msgs, pc, precomputedTcIdx) {
2549
+ const tcIdx = precomputedTcIdx ?? buildToolCallIndex(msgs);
2550
+ const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
2551
+ const errors = catalogErrors(msgs, tcIdx);
2552
+ const decisions = extractDecisions(msgs, tcIdx);
2553
+ const constraints = mineConstraints(msgs);
2554
+ const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
2555
+ const timeline = buildTimeline(msgs, errors);
2556
+ const mediaAttachments = extractMediaAttachments(msgs);
2557
+ const mainGoal = extractMainGoal(msgs);
2558
+ const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
2559
+ const lastErrors = errors.slice(-3).map((e) => e.message);
2560
+ return {
2561
+ modifiedFiles: modified,
2562
+ readFiles: read,
2563
+ deletedFiles: deleted,
2564
+ errors,
2565
+ decisions,
2566
+ constraints,
2567
+ topics,
2568
+ timeline,
2569
+ mediaAttachments,
2570
+ mainGoal,
2571
+ lastUserMessages,
2572
+ lastErrors,
2573
+ messageCount: msgs.length
2602
2574
  };
2603
- return isValidSmartCompactDetails(repaired) ? repaired : null;
2604
2575
  }
2605
2576
 
2606
- // src/utils/file-needles.ts
2607
- var GENERIC_BASENAMES = new Set([
2608
- "index.ts",
2609
- "index.js",
2610
- "index.tsx",
2611
- "index.jsx",
2612
- "types.ts",
2613
- "helpers.ts",
2614
- "utils.ts",
2615
- "main.ts",
2616
- "main.js",
2617
- "mod.rs",
2618
- "lib.rs",
2619
- "__init__.py"
2620
- ]);
2621
- var MIN_BARE_BASENAME_LEN = 5;
2622
- function buildPathNeedles(filePath) {
2623
- const parts = filePath.toLowerCase().split("/").filter(Boolean);
2624
- if (parts.length === 0)
2625
- return [];
2626
- const needles = [];
2627
- const basename = parts[parts.length - 1];
2628
- if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
2629
- needles.push(basename);
2577
+ // src/utils/helpers.ts
2578
+ init_constants();
2579
+ var VALID_PROFILES = ["light", "balanced", "aggressive"];
2580
+ var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
2581
+ function validateSmartCompactConfig(sc) {
2582
+ if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
2583
+ warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
2584
+ delete sc.profile;
2630
2585
  }
2631
- for (let j = parts.length - 2;j >= 0; j--) {
2632
- needles.push(parts.slice(j).join("/"));
2586
+ if ("autoTrigger" in sc && typeof sc.autoTrigger !== "boolean") {
2587
+ warn("smart-compact config: autoTrigger must be boolean, got " + typeof sc.autoTrigger);
2588
+ delete sc.autoTrigger;
2589
+ }
2590
+ if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
2591
+ warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
2592
+ delete sc.backupEnabled;
2593
+ }
2594
+ if ("summaryModel" in sc && sc.summaryModel !== null && typeof sc.summaryModel !== "string") {
2595
+ warn("smart-compact config: summaryModel must be string|null, got " + typeof sc.summaryModel);
2596
+ delete sc.summaryModel;
2597
+ }
2598
+ if ("segmentationModel" in sc && sc.segmentationModel !== null && typeof sc.segmentationModel !== "string") {
2599
+ warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
2600
+ delete sc.segmentationModel;
2601
+ }
2602
+ if ("profiles" in sc) {
2603
+ if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
2604
+ warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
2605
+ delete sc.profiles;
2606
+ } else {
2607
+ const profiles = sc.profiles;
2608
+ for (const [profileName, value] of Object.entries(profiles)) {
2609
+ if (!VALID_PROFILES.includes(profileName)) {
2610
+ warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
2611
+ delete profiles[profileName];
2612
+ continue;
2613
+ }
2614
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2615
+ warn("smart-compact config: profile '" + profileName + "' must be an object.");
2616
+ delete profiles[profileName];
2617
+ continue;
2618
+ }
2619
+ const profileCfg = value;
2620
+ for (const [key, raw] of Object.entries(profileCfg)) {
2621
+ if (!PROFILE_NUMERIC_KEYS.includes(key)) {
2622
+ warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
2623
+ delete profileCfg[key];
2624
+ continue;
2625
+ }
2626
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
2627
+ warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
2628
+ delete profileCfg[key];
2629
+ }
2630
+ }
2631
+ }
2632
+ }
2633
+ }
2634
+ if ("autoTriggerTimeoutMs" in sc) {
2635
+ const v = sc.autoTriggerTimeoutMs;
2636
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
2637
+ warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
2638
+ delete sc.autoTriggerTimeoutMs;
2639
+ }
2640
+ }
2641
+ if ("minContextPercent" in sc) {
2642
+ const v = sc.minContextPercent;
2643
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
2644
+ warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
2645
+ delete sc.minContextPercent;
2646
+ }
2647
+ }
2648
+ if ("backupDir" in sc && sc.backupDir !== undefined && typeof sc.backupDir !== "string") {
2649
+ warn("smart-compact config: backupDir must be a string, got " + typeof sc.backupDir + ". Using default.");
2650
+ delete sc.backupDir;
2651
+ }
2652
+ if ("pinPaths" in sc && sc.pinPaths !== undefined) {
2653
+ if (!Array.isArray(sc.pinPaths) || !sc.pinPaths.every((x) => typeof x === "string")) {
2654
+ warn("smart-compact config: pinPaths must be a string[], ignoring.");
2655
+ delete sc.pinPaths;
2656
+ }
2633
2657
  }
2634
- return needles;
2635
- }
2636
-
2637
- // src/utils/extraction.ts
2638
- var TRUNCATE_RE = /\u2026\u2702\d+$/;
2639
- function isTruncated(content) {
2640
- return TRUNCATE_RE.test(extractText(content));
2641
- }
2642
- var WRITE_TOOL_HINTS = ["write", "edit", "patch", "create", "append", "update", "apply"];
2643
- var DELETE_TOOL_HINTS = ["delete", "remove", "unlink"];
2644
- var READ_TOOL_HINTS = ["read", "view", "open"];
2645
- function hasToolHint(tool, hints) {
2646
- return hints.some((hint) => tool.includes(hint));
2647
2658
  }
2648
- function flattenToolCallBlock(b) {
2649
- if (!isToolCallBlock(b))
2650
- return [];
2651
- if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2652
- return b.arguments.tool_uses.map((u) => {
2653
- const recipient = u?.recipient_name ?? "";
2654
- return {
2655
- name: recipient.replace(/^functions\./, ""),
2656
- id: u?.id ?? undefined,
2657
- arguments: u?.parameters ?? {}
2658
- };
2659
- });
2659
+ var _cfg = null;
2660
+ var _cfgMtime = 0;
2661
+ var _cfgPath = null;
2662
+ function loadConfig() {
2663
+ try {
2664
+ const p = settingsFile();
2665
+ const stat = fs2.statSync(p);
2666
+ if (_cfg && _cfgPath === p && stat.mtimeMs === _cfgMtime)
2667
+ return _cfg;
2668
+ const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
2669
+ const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
2670
+ validateSmartCompactConfig(sc);
2671
+ const merged = { ...DEFAULT_CONFIG, ...sc };
2672
+ if (sc.profiles)
2673
+ merged.profiles = { ...PROFILES, ...sc.profiles };
2674
+ if (!merged.backupDir)
2675
+ merged.backupDir = defaultBackupDir();
2676
+ _cfg = merged;
2677
+ _cfgMtime = stat.mtimeMs;
2678
+ _cfgPath = p;
2679
+ return _cfg;
2680
+ } catch (e) {
2681
+ debug("loadConfig: settings.json not found or unreadable, using defaults", e);
2682
+ const fallback = { ...DEFAULT_CONFIG, backupDir: defaultBackupDir() };
2683
+ _cfg = fallback;
2684
+ _cfgPath = null;
2685
+ return fallback;
2660
2686
  }
2661
- return [{ name: b.name, id: b.id, arguments: b.arguments }];
2662
- }
2663
- function extractText(content) {
2664
- if (typeof content === "string")
2665
- return content;
2666
- if (!Array.isArray(content))
2667
- return "";
2668
- return content.map((b) => {
2669
- if (typeof b === "string")
2670
- return b;
2671
- if (isTextBlock(b))
2672
- return b.text;
2673
- return "";
2674
- }).join("");
2675
- }
2676
- function mediaKind(type, mime) {
2677
- const s = (type + " " + (mime ?? "")).toLowerCase();
2678
- if (/image|input_image|image_url/.test(s))
2679
- return "image";
2680
- if (/audio/.test(s))
2681
- return "audio";
2682
- if (/video/.test(s))
2683
- return "video";
2684
- if (/file|document|pdf|attachment/.test(s))
2685
- return "file";
2686
- return "unknown";
2687
2687
  }
2688
- function extractMediaAttachments(msgs) {
2689
- const out = [];
2690
- for (let i = 0;i < msgs.length; i++) {
2691
- const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
2692
- for (const b of blocks) {
2693
- if (!b || typeof b !== "object")
2694
- continue;
2695
- const rec = b;
2696
- const type = String(rec.type ?? "");
2697
- if (type === "text" || type === "toolCall" || type === "tool_use")
2698
- continue;
2699
- const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
2700
- const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
2701
- const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
2702
- const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
2703
- const kind = mediaKind(type, mimeType);
2704
- if (kind !== "unknown" || source || mimeType || name) {
2705
- out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
2688
+ var _pruneInFlight = new Set;
2689
+ function prunePass(dir) {
2690
+ try {
2691
+ const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => {
2692
+ const full = path4.join(dir, name);
2693
+ try {
2694
+ return { full, mtimeMs: fs2.statSync(full).mtimeMs };
2695
+ } catch {
2696
+ return null;
2697
+ }
2698
+ }).filter((v) => v !== null);
2699
+ const now = Date.now();
2700
+ const overAge = entries.filter((e) => now - e.mtimeMs > BACKUP_MAX_AGE_MS);
2701
+ const sorted = entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
2702
+ const overCount = sorted.slice(BACKUP_MAX_FILES);
2703
+ const toRemove = new Set([...overAge, ...overCount].map((e) => e.full));
2704
+ for (const full of toRemove) {
2705
+ try {
2706
+ fs2.unlinkSync(full);
2707
+ } catch (e) {
2708
+ debug("prunePass unlink failed", e);
2706
2709
  }
2707
2710
  }
2711
+ } catch (e) {
2712
+ debug("prunePass scan failed", e);
2708
2713
  }
2709
- return out;
2710
2714
  }
2711
- function buildToolCallIndex(msgs) {
2712
- const idx = new Map;
2713
- for (let i = 0;i < msgs.length; i++) {
2714
- const m = msgs[i];
2715
- if (m.role !== "assistant")
2716
- continue;
2717
- const blocks = Array.isArray(m.content) ? m.content : [];
2718
- for (const b of blocks) {
2719
- if (!isToolCallBlock(b))
2720
- continue;
2721
- if (b.id) {
2722
- idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
2723
- }
2724
- if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2725
- const nested = flattenToolCallBlock(b);
2726
- for (let t = 0;t < nested.length; t++) {
2727
- const tool = nested[t];
2728
- const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
2729
- idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
2730
- }
2731
- }
2715
+ function schedulePruneBackups(dir) {
2716
+ if (_pruneInFlight.has(dir))
2717
+ return;
2718
+ _pruneInFlight.add(dir);
2719
+ queueMicrotask(() => {
2720
+ try {
2721
+ prunePass(dir);
2722
+ } finally {
2723
+ _pruneInFlight.delete(dir);
2732
2724
  }
2725
+ });
2726
+ }
2727
+ function backupConversation(convText, sessionId) {
2728
+ try {
2729
+ const cfg = loadConfig();
2730
+ if (!cfg.backupEnabled)
2731
+ return null;
2732
+ const dir = cfg.backupDir;
2733
+ ensureDir(dir);
2734
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
2735
+ const hash = crypto2.createHash("sha256").update(convText).digest("hex").slice(0, TRUNC.CONV_HASH);
2736
+ const fp = path4.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
2737
+ atomicWriteFileSync(fp, `# Smart Compact Backup
2738
+ # Date: ` + new Date().toISOString() + `
2739
+ # Session: ` + sessionId + `
2740
+
2741
+ ` + convText);
2742
+ schedulePruneBackups(dir);
2743
+ return fp;
2744
+ } catch (e) {
2745
+ warn("backupConversation failed", e);
2746
+ return null;
2733
2747
  }
2734
- return idx;
2735
2748
  }
2736
- function trackFileOps(msgs, _tcIdx) {
2737
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2738
- const modMap = new Map;
2739
- const readSet = new Set;
2740
- const delSet = new Set;
2741
- for (let i = 0;i < msgs.length; i++) {
2742
- const m = msgs[i];
2743
- if (m.role !== "toolResult" || m.isError)
2744
- continue;
2745
- const tc = tcIdx.get(m.toolCallId ?? "");
2746
- if (!tc)
2747
- continue;
2748
- const args = tc.arguments;
2749
- const filePath = args?.path ?? args?.file_path ?? args?.filePath;
2750
- if (!filePath)
2751
- continue;
2752
- const tool = tc.name.toLowerCase();
2753
- if (hasToolHint(tool, WRITE_TOOL_HINTS)) {
2754
- const resultText = extractText(m.content);
2755
- if (isTruncated(resultText)) {
2756
- const existing = modMap.get(filePath);
2757
- modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
2758
- } else if (!NO_OP_RE.test(resultText)) {
2759
- const existing = modMap.get(filePath);
2760
- modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
2761
- }
2762
- } else if (hasToolHint(tool, DELETE_TOOL_HINTS)) {
2763
- delSet.add(filePath);
2764
- } else if (hasToolHint(tool, READ_TOOL_HINTS)) {
2765
- readSet.add(filePath);
2749
+ function listBackups(limit = 20) {
2750
+ try {
2751
+ const dir = loadConfig().backupDir;
2752
+ if (!fs2.existsSync(dir))
2753
+ return [];
2754
+ const out = [];
2755
+ for (const name of fs2.readdirSync(dir)) {
2756
+ if (!name.endsWith(".md"))
2757
+ continue;
2758
+ const full = path4.join(dir, name);
2759
+ try {
2760
+ const stat = fs2.statSync(full);
2761
+ if (!stat.isFile())
2762
+ continue;
2763
+ const head = fs2.readFileSync(full, "utf-8").split(`
2764
+ `).slice(0, TRUNC.BACKUP_PREVIEW_LINES).join(`
2765
+ `);
2766
+ const date = head.match(/^# Date:\s*(.+)$/m)?.[1]?.trim();
2767
+ const session = head.match(/^# Session:\s*(.+)$/m)?.[1]?.trim();
2768
+ out.push({
2769
+ path: full,
2770
+ sessionId: session ?? name,
2771
+ date: date ?? stat.mtime.toISOString(),
2772
+ sizeBytes: stat.size
2773
+ });
2774
+ } catch {}
2766
2775
  }
2776
+ out.sort((a, b) => a.date < b.date ? 1 : a.date > b.date ? -1 : 0);
2777
+ return out.slice(0, limit);
2778
+ } catch (e) {
2779
+ warn("listBackups failed", e);
2780
+ return [];
2781
+ }
2782
+ }
2783
+ function readBackupContent(fp) {
2784
+ try {
2785
+ const raw = fs2.readFileSync(fp, "utf-8");
2786
+ const lines = raw.split(`
2787
+ `);
2788
+ let i = 0;
2789
+ while (i < lines.length && lines[i].startsWith("#"))
2790
+ i++;
2791
+ if (i < lines.length && lines[i].trim() === "")
2792
+ i++;
2793
+ return lines.slice(i).join(`
2794
+ `).trim() || null;
2795
+ } catch (e) {
2796
+ warn("readBackupContent failed", e);
2797
+ return null;
2767
2798
  }
2799
+ }
2800
+ function buildRestoreMessage(content, source) {
2768
2801
  return {
2769
- modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
2770
- read: [...readSet],
2771
- deleted: [...delSet]
2802
+ customType: "smart-compact-restore",
2803
+ content: `# Restored pre-compaction context (smart-compact backup)
2804
+ Source: ` + source + `
2805
+
2806
+ ` + content,
2807
+ display: true,
2808
+ details: { source, restoredAt: Date.now() }
2772
2809
  };
2773
2810
  }
2774
- function catalogErrors(msgs, _tcIdx) {
2775
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2776
- const errors = [];
2777
- for (let i = 0;i < msgs.length; i++) {
2778
- const m = msgs[i];
2779
- if (m.role !== "toolResult")
2811
+ function getPreviousCompactionContext(branch) {
2812
+ const compactions = branch.filter((e) => e.type === "compaction");
2813
+ if (!compactions.length)
2814
+ return "";
2815
+ const last = compactions[compactions.length - 1];
2816
+ const topics = last.details?.topics ?? [];
2817
+ if (!topics.length)
2818
+ return "";
2819
+ return `
2820
+ [IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
2821
+ }
2822
+ function findLastAnchorIndex(branchEntries) {
2823
+ for (let i = branchEntries.length - 1;i >= 0; i--) {
2824
+ const e = branchEntries[i];
2825
+ if (e?.type !== "message")
2780
2826
  continue;
2781
- const tc = tcIdx.get(m.toolCallId ?? "");
2782
- if (m.isError) {
2783
- errors.push({ index: i, tool: tc?.name ?? "unknown", message: extractText(m.content).slice(0, 500), retryAttempted: false, resolved: false });
2827
+ const msg = e.message;
2828
+ if (msg?.role !== "toolResult")
2784
2829
  continue;
2785
- }
2786
- if (tc?.name === "bash") {
2787
- const txt = extractText(m.content);
2788
- const isLikelyError = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i.test(txt);
2789
- if (isLikelyError && txt.length < 2000) {
2790
- errors.push({ index: i, tool: "bash", message: txt.slice(0, 300), retryAttempted: false, resolved: false });
2791
- }
2830
+ if (msg?.toolName === "context" && msg?.details?.anchor) {
2831
+ return i;
2792
2832
  }
2793
2833
  }
2794
- for (const err of errors) {
2795
- for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
2796
- if (msgs[j]?.role === "assistant") {
2797
- const rawBlocks = msgs[j]?.content;
2798
- const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
2799
- for (const b of blocks) {
2800
- for (const tool of flattenToolCallBlock(b)) {
2801
- if (tool.name === err.tool) {
2802
- err.retryAttempted = true;
2803
- for (let k = j + 1;k < Math.min(msgs.length, j + 10); k++) {
2804
- if (msgs[k]?.role === "toolResult" && msgs[k]?.toolCallId === tool.id && !msgs[k]?.isError) {
2805
- err.resolved = true;
2806
- break;
2807
- }
2808
- }
2809
- break;
2810
- }
2811
- }
2812
- if (err.retryAttempted)
2813
- break;
2814
- }
2815
- if (err.retryAttempted)
2816
- break;
2817
- }
2834
+ return -1;
2835
+ }
2836
+ function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
2837
+ let msgCount = 0;
2838
+ for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
2839
+ const e = branchEntries[i];
2840
+ if (e?.type === "message") {
2841
+ if (msgCount >= msgs.length)
2842
+ return msgs.length - 1;
2843
+ msgCount++;
2818
2844
  }
2819
2845
  }
2820
- return errors;
2846
+ return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
2821
2847
  }
2822
- function extractDecisions(msgs, _tcIdx) {
2823
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2824
- const decisions = [];
2825
- for (const [id, tc] of tcIdx) {
2826
- if (tc.name !== "ask_user")
2827
- continue;
2828
- const args = tc.arguments;
2829
- const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
2830
- if (!question)
2831
- continue;
2832
- for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
2833
- if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
2834
- decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, 200), userResponse: extractText(msgs[i].content).slice(0, 300) });
2835
- break;
2848
+ function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
2849
+ let adjusted = keepFromIndex;
2850
+ if (branchEntries && branchEntries.length > 0) {
2851
+ const lastAnchorBranchIdx = findLastAnchorIndex(branchEntries);
2852
+ if (lastAnchorBranchIdx >= 0) {
2853
+ const lastAnchorMsgIdx = branchIndexToMsgIndex(branchEntries, lastAnchorBranchIdx, msgs);
2854
+ if (adjusted > lastAnchorMsgIdx && lastAnchorMsgIdx >= 0) {
2855
+ adjusted = lastAnchorMsgIdx;
2836
2856
  }
2837
2857
  }
2838
2858
  }
2839
- for (let i = 0;i < msgs.length; i++) {
2840
- if (msgs[i]?.role !== "user")
2841
- continue;
2842
- const txt = extractText(msgs[i].content);
2843
- if (CHOICE_RE.test(txt)) {
2844
- decisions.push({ index: i, type: "implicit", summary: txt.slice(0, 200) });
2845
- }
2846
- }
2847
- return decisions;
2848
- }
2849
- var CONSTRAINT_PATTERNS = [
2850
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: 0.85 },
2851
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: 0.8 },
2852
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: 0.6 },
2853
- { re: /\b(?:kritik|kritikal|\u00f6nemli|onemli|\u015fart|sart|zorunlu|\u015fart ko\u015ful|\u00f6nemli \u015fart|kesinlikle|kesinlikle \u015fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|b\u00f6yle olsun|b\u00f6yle yap\u0131n|\u015f\u00f6yle olsun|\u015f\u00f6yle yap\u0131n)\b/iu, cat: "requirement", conf: 0.8 },
2854
- { re: /\b(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))\b/iu, cat: "prohibition", conf: 0.8 },
2855
- { re: /\b(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)\b/iu, cat: "preference", conf: 0.6 }
2856
- ];
2857
- function mineConstraints(msgs) {
2858
- const constraints = [];
2859
- for (let i = 0;i < msgs.length; i++) {
2860
- if (msgs[i]?.role !== "user")
2861
- continue;
2862
- const txt = extractText(msgs[i].content);
2863
- if (txt.length < 10 || txt.startsWith("/"))
2864
- continue;
2865
- for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
2866
- if (re.test(txt)) {
2867
- constraints.push({ index: i, text: txt.slice(0, 300), category: cat, confidence: conf });
2868
- break;
2859
+ if (adjusted <= 0 || adjusted >= msgs.length)
2860
+ return adjusted;
2861
+ const touchedFiles = (msg) => {
2862
+ const m = msg;
2863
+ const blocks = Array.isArray(m?.content) ? m.content : [];
2864
+ const files = new Set;
2865
+ for (const b of blocks) {
2866
+ for (const tc of flattenToolCallBlock(b)) {
2867
+ const fp = extractToolPath(tc.arguments);
2868
+ if (fp)
2869
+ files.add(fp.split("/").pop() ?? fp);
2869
2870
  }
2870
2871
  }
2872
+ return files;
2873
+ };
2874
+ const lastFiles = touchedFiles(msgs[adjusted - 1].message);
2875
+ if (lastFiles.size > 0) {
2876
+ const keptFiles = touchedFiles(msgs[adjusted].message);
2877
+ if ([...lastFiles].some((f) => keptFiles.has(f)))
2878
+ return adjusted - 1;
2871
2879
  }
2872
- return constraints;
2880
+ return adjusted;
2873
2881
  }
2874
- function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2875
- const topics = [];
2876
- let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
2877
- let currentType = "exploration";
2878
- let currentPrimaryFile = null;
2879
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2880
- for (let i = 0;i < msgs.length; i++) {
2881
- const m = msgs[i];
2882
- const txt = extractText(m.content);
2883
- tokenAcc += estimateTokens(txt);
2884
- let brk = false;
2885
- let type = "exploration";
2886
- let primaryFile = null;
2887
- if (m.role === "assistant") {
2888
- const blocks = Array.isArray(m.content) ? m.content : [];
2889
- for (const b of blocks) {
2890
- for (const tool of flattenToolCallBlock(b)) {
2891
- const fp = tool.arguments?.path ?? tool.arguments?.file_path;
2892
- if (!fp)
2893
- continue;
2894
- const fn = path6.basename(fp);
2895
- if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
2896
- brk = true;
2897
- lastFile = fn;
2898
- primaryFile = fp;
2899
- currentPrimaryFile = fp;
2900
- if (tool.name?.includes("write") || tool.name?.includes("edit")) {
2901
- type = "implementation";
2902
- if (currentType !== "implementation")
2903
- currentType = "implementation";
2904
- } else if (tool.name?.includes("read")) {
2905
- type = "review";
2906
- if (currentType === "exploration")
2907
- currentType = "review";
2882
+ function collectToolCallIds(blocks, msgIndex, out) {
2883
+ for (const b of blocks) {
2884
+ const block = b;
2885
+ if (block?.type === "toolCall") {
2886
+ if (typeof block.id === "string") {
2887
+ out.set(block.id, msgIndex);
2888
+ }
2889
+ const args = block.arguments;
2890
+ if (block.name === "multi_tool_use.parallel" && args && Array.isArray(args.tool_uses)) {
2891
+ for (const nested of args.tool_uses) {
2892
+ const n = nested;
2893
+ if (typeof n.id === "string") {
2894
+ out.set(n.id, msgIndex);
2908
2895
  }
2909
2896
  }
2910
2897
  }
2911
2898
  }
2912
- if (m.role === "toolResult" && m.isError) {
2913
- errAcc++;
2914
- type = "debugging";
2915
- if (currentType !== "implementation")
2916
- currentType = "debugging";
2899
+ }
2900
+ }
2901
+ function guardToolCallBoundary(msgs, keepFrom) {
2902
+ if (keepFrom <= 0 || keepFrom >= msgs.length)
2903
+ return keepFrom;
2904
+ const tcMap = new Map;
2905
+ for (let i = 0;i < msgs.length; i++) {
2906
+ const m = msgs[i].message;
2907
+ if (m?.role !== "assistant")
2908
+ continue;
2909
+ const blocks = Array.isArray(m?.content) ? m.content : [];
2910
+ collectToolCallIds(blocks, i, tcMap);
2911
+ }
2912
+ let adjusted = keepFrom;
2913
+ let changed = true;
2914
+ const MAX_ITER = msgs.length + 1;
2915
+ let iter = 0;
2916
+ while (changed) {
2917
+ if (++iter > MAX_ITER) {
2918
+ warn("guardToolCallBoundary hit MAX_ITER=" + MAX_ITER + " at adjusted=" + adjusted);
2919
+ break;
2917
2920
  }
2918
- if (m.role === "toolResult" && !m.isError) {
2919
- const tc = tcIdx.get(m.toolCallId ?? "");
2920
- if (tc?.name === "bash" && /error|fail/i.test(txt)) {
2921
- errAcc++;
2922
- type = "debugging";
2923
- if (currentType !== "implementation")
2924
- currentType = "debugging";
2921
+ changed = false;
2922
+ for (let i = adjusted;i < msgs.length; i++) {
2923
+ const m = msgs[i].message;
2924
+ if (m?.role !== "toolResult")
2925
+ continue;
2926
+ const tcId = m?.toolCallId;
2927
+ if (!tcId)
2928
+ continue;
2929
+ const tcIdx = tcMap.get(tcId);
2930
+ if (tcIdx !== undefined && tcIdx < adjusted) {
2931
+ adjusted = tcIdx;
2932
+ changed = true;
2933
+ break;
2925
2934
  }
2926
2935
  }
2927
- if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
2928
- brk = true;
2929
- if (tokenAcc >= pc.maxChunkTokens)
2930
- brk = true;
2931
- if (brk && i > startIdx && topics.length < maxSegs - 1) {
2932
- topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2933
- startIdx = i + 1;
2934
- tokenAcc = 0;
2935
- lastFile = null;
2936
- errAcc = 0;
2937
- currentType = "exploration";
2938
- currentPrimaryFile = null;
2939
- }
2940
2936
  }
2941
- if (startIdx < msgs.length) {
2942
- topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2943
- }
2944
- return topics;
2937
+ return Math.max(0, Math.min(adjusted, msgs.length));
2945
2938
  }
2946
- function buildTimeline(msgs, errors) {
2947
- const timeline = [];
2948
- const errorIndices = new Set(errors.map((e) => e.index));
2949
- for (let i = 0;i < msgs.length; i++) {
2950
- const m = msgs[i];
2951
- if (m.role === "user") {
2952
- const txt = extractText(m.content);
2953
- if (!txt.startsWith("/"))
2954
- timeline.push({ index: i, event: "user_request", summary: txt.slice(0, 150) });
2939
+ function extractUserNote(args) {
2940
+ const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
2941
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
2942
+ const isOptionToken = (t) => t.startsWith("--") || SKIP.has(t.toLowerCase()) || /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t);
2943
+ const nonFlags = tokens.filter((t) => !isOptionToken(t));
2944
+ return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
2945
+ }
2946
+ function createBatches(chunks, maxTokens) {
2947
+ const batches = [];
2948
+ let batch = [], bt = 0;
2949
+ for (const ch of chunks) {
2950
+ if (batch.length && bt + ch.tokenEstimate > maxTokens) {
2951
+ batches.push(batch);
2952
+ batch = [];
2953
+ bt = 0;
2955
2954
  }
2956
- if (errorIndices.has(i))
2957
- timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, 100) ?? "error" });
2955
+ batch.push(ch);
2956
+ bt += ch.tokenEstimate;
2957
+ }
2958
+ if (batch.length)
2959
+ batches.push(batch);
2960
+ return batches;
2961
+ }
2962
+ function allocateTopicBudgets(summaries, totalBudget) {
2963
+ const n = summaries.length;
2964
+ if (n === 0)
2965
+ return new Map;
2966
+ const weights = summaries.map((s, i) => {
2967
+ let w = 1;
2968
+ if (s.priority === "critical")
2969
+ w *= 2;
2970
+ else if (s.priority === "high")
2971
+ w *= 1.5;
2972
+ else if (s.priority === "low")
2973
+ w *= 0.6;
2974
+ const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
2975
+ w *= 1 + errorKeywords * 0.2;
2976
+ const recency = (i + 1) / n;
2977
+ w *= 0.6 + recency * 0.4;
2978
+ if (s.keyDecisions.length > 0)
2979
+ w *= 1.3;
2980
+ return w;
2981
+ });
2982
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
2983
+ const baseTokensPerTopic = Math.floor(totalBudget / n);
2984
+ const budgetMap = new Map;
2985
+ for (let i = 0;i < summaries.length; i++) {
2986
+ const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
2987
+ budgetMap.set(summaries[i].topic, Math.max(200, allocated));
2958
2988
  }
2959
- return timeline.length > 30 ? [...timeline.filter((t) => t.event === "user_request").slice(0, 10), ...timeline.filter((t) => t.event === "error")] : timeline;
2989
+ return budgetMap;
2990
+ }
2991
+ function preProcessSummaries(summaries, budgetTokens) {
2992
+ const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
2993
+ return {
2994
+ decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
2995
+ modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
2996
+ read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
2997
+ text: summaries.map((cs, i) => {
2998
+ const budgetHint = topicBudgets?.get(cs.topic);
2999
+ const budgetLine = budgetHint ? `
3000
+ Budget: ~` + budgetHint + " tokens" : "";
3001
+ return "### Segment " + (i + 1) + ": " + cs.topic + `
3002
+ Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
3003
+
3004
+ ` + cs.summary + `
3005
+
3006
+ Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
3007
+ Modified: ` + (cs.filesModified.join(", ") || "None") + `
3008
+ Read: ` + (cs.filesRead.join(", ") || "None");
3009
+ }).join(`
3010
+ ---
3011
+ `)
3012
+ };
2960
3013
  }
2961
- function extractMainGoal(msgs) {
2962
- for (const m of msgs) {
2963
- if (m?.role !== "user")
2964
- continue;
2965
- const txt = extractText(m.content).trim();
2966
- if (txt && !txt.startsWith("/"))
2967
- return txt.slice(0, 300);
2968
- }
2969
- return null;
3014
+ function buildExtractionContext(extraction, forRange) {
3015
+ const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
3016
+ const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
3017
+ const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
3018
+ return [
3019
+ "## Deterministic Extraction (verified facts)",
3020
+ "Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
3021
+ "Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, TRUNC.SNIPPET) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
3022
+ "Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, TRUNC.DECISION_DETAIL)).join("; ") || "none"),
3023
+ "Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, TRUNC.DECISION_DETAIL)).join("; ") || "none"),
3024
+ "Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
3025
+ ].join(`
3026
+ `);
2970
3027
  }
2971
- function extractOpenLoops(msgs, extraction) {
2972
- const loops = [];
2973
- let loopId = 0;
2974
- const fileNeedles = extraction.modifiedFiles.map((f) => ({
2975
- path: f.path,
2976
- needles: buildPathNeedles(f.path)
2977
- }));
2978
- for (const err of extraction.errors.filter((e) => !e.resolved)) {
2979
- const errLower = err.message.toLowerCase();
2980
- const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path7 }) => path7);
2981
- loops.push({
2982
- id: "loop-" + ++loopId,
2983
- type: "bugfix",
2984
- priority: err.retryAttempted ? "high" : "normal",
2985
- status: "open",
2986
- summary: err.message.slice(0, 120),
2987
- files: errFiles,
2988
- sourceIndex: err.index
2989
- });
2990
- }
2991
- const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|yapalim|yapal\u0131m|yapmamiz|yapmam\u0131z|gerekiyor|eklenecek|d\u00FCzeltilecek|duzeltilecek|bitmedi|kaldi|kald\u0131)/iu;
2992
- for (let idx = 0;idx < msgs.length; idx++) {
2993
- const msg = msgs[idx];
2994
- if (msg.role !== "user")
2995
- continue;
2996
- const txt = extractText(msg.content);
2997
- if (txt.length < 10 || txt.startsWith("/"))
3028
+ function computeToolCharPercentage(branchEntries) {
3029
+ let totalChars = 0;
3030
+ let toolChars = 0;
3031
+ for (const raw of branchEntries) {
3032
+ const m = raw?.message;
3033
+ if (!m)
2998
3034
  continue;
2999
- if (FOLLOWUP_RE.test(txt)) {
3000
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
3001
- if (!isDup) {
3002
- loops.push({
3003
- id: "loop-" + ++loopId,
3004
- type: "follow-up",
3005
- priority: "normal",
3006
- status: "open",
3007
- summary: txt.slice(0, 120),
3008
- files: [],
3009
- sourceIndex: idx
3010
- });
3035
+ let mc = 0;
3036
+ if (typeof m.content === "string") {
3037
+ mc = m.content.length;
3038
+ } else if (Array.isArray(m.content)) {
3039
+ for (const part of m.content) {
3040
+ if (!part || typeof part !== "object")
3041
+ continue;
3042
+ const block = part;
3043
+ if (block.type === "text" && typeof block.text === "string")
3044
+ mc += block.text.length;
3011
3045
  }
3012
3046
  }
3047
+ totalChars += mc;
3048
+ if (m.role === "toolResult")
3049
+ toolChars += mc;
3013
3050
  }
3014
- const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
3015
- for (let idx = 0;idx < msgs.length; idx++) {
3016
- const msg = msgs[idx];
3017
- if (msg.role !== "user")
3018
- continue;
3019
- const txt = extractText(msg.content);
3020
- if (BLOCKED_RE.test(txt)) {
3021
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
3022
- if (!isDup) {
3023
- loops.push({
3024
- id: "loop-" + ++loopId,
3025
- type: "blocked",
3026
- priority: "high",
3027
- status: "open",
3028
- summary: txt.slice(0, 120),
3029
- files: [],
3030
- sourceIndex: idx
3031
- });
3032
- }
3033
- }
3051
+ return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
3052
+ }
3053
+ function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
3054
+ if (totalTokens < minThreshold)
3055
+ return "none";
3056
+ if (contextPercent < minContextPercent)
3057
+ return "none";
3058
+ if (contextPercent < 80)
3059
+ return "light";
3060
+ return "full";
3061
+ }
3062
+ function inferSessionType(extraction, report) {
3063
+ if (report?.sessionType)
3064
+ return report.sessionType;
3065
+ const hasModifications = extraction.modifiedFiles.length > 0;
3066
+ const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
3067
+ const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
3068
+ const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
3069
+ const hasDecisions = extraction.decisions.length > 0;
3070
+ if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
3071
+ return "debugging";
3072
+ if (hasReadsOnly && !hasDecisions)
3073
+ return "review";
3074
+ if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
3075
+ return "discussion";
3076
+ if (hasModifications)
3077
+ return "implementation";
3078
+ return "implementation";
3079
+ }
3080
+ function buildExplorationContext(report) {
3081
+ if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
3082
+ return "";
3083
+ return [
3084
+ "## Exploration Report",
3085
+ "Main goal: " + report.mainGoal,
3086
+ "Session type: " + report.sessionType,
3087
+ report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
3088
+ report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
3089
+ report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
3090
+ report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
3091
+ report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
3092
+ ].filter(Boolean).join(`
3093
+ `);
3094
+ }
3095
+
3096
+ // src/index.ts
3097
+ init_tokens();
3098
+ init_cache();
3099
+
3100
+ // src/app/run-smart-compact.ts
3101
+ init_overlays();
3102
+ init_logger();
3103
+
3104
+ // src/app/run-context.ts
3105
+ function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
3106
+ rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
3107
+ rc.phaseStart = endMs;
3108
+ }
3109
+ function markPhase(rc, phase) {
3110
+ markMeasuredPhase(rc, phase, rc.phaseStart);
3111
+ }
3112
+ function advance(rc, _stage) {
3113
+ return rc;
3114
+ }
3115
+
3116
+ // src/app/run-smart-compact.ts
3117
+ init_services();
3118
+
3119
+ // src/app/steps/prepare.ts
3120
+ init_constants();
3121
+ init_tokens();
3122
+ init_logger();
3123
+ async function prepareRun(rc) {
3124
+ const config = loadConfig();
3125
+ const profileCfg = { ...PROFILES[rc.profile], ...config.profiles?.[rc.profile] ?? {} };
3126
+ const providerCaps = getProviderCaps(rc.summaryModel.provider);
3127
+ const auth = await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.summaryModel);
3128
+ const segAuth = rc.segModel !== rc.summaryModel ? await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.segModel) : auth;
3129
+ if (!auth.ok || !auth.apiKey || (!segAuth.ok || !segAuth.apiKey)) {
3130
+ if (!rc.flags.autoTriggered)
3131
+ rc.ctx.ui.notify("Auth failed", "error");
3132
+ return null;
3034
3133
  }
3035
- for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
3036
- const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
3037
- if (!exists) {
3038
- loops.push({
3039
- id: "loop-" + ++loopId,
3040
- type: "retry",
3041
- priority: "high",
3042
- status: "open",
3043
- summary: "Retried but unresolved: " + err.message.slice(0, 80),
3044
- files: [],
3045
- sourceIndex: err.index
3046
- });
3047
- }
3134
+ if (rc.timeoutMs > 0) {
3135
+ rc.cancellation.timeoutId = setTimeout(() => {
3136
+ rc.cancellation.timedOut = true;
3137
+ rc.cancellation.controller.abort();
3138
+ rc.notify("Smart compact auto-trigger exceeded " + rc.timeoutMs + "ms; Pi will use native compact for this run", "warning");
3139
+ }, rc.timeoutMs);
3048
3140
  }
3049
- return loops;
3050
- }
3051
- function extractStructured(msgs, pc, precomputedTcIdx) {
3052
- const tcIdx = precomputedTcIdx ?? buildToolCallIndex(msgs);
3053
- const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
3054
- const errors = catalogErrors(msgs, tcIdx);
3055
- const decisions = extractDecisions(msgs, tcIdx);
3056
- const constraints = mineConstraints(msgs);
3057
- const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
3058
- const timeline = buildTimeline(msgs, errors);
3059
- const mediaAttachments = extractMediaAttachments(msgs);
3060
- const mainGoal = extractMainGoal(msgs);
3061
- const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
3062
- const lastErrors = errors.slice(-3).map((e) => e.message);
3063
- return {
3064
- modifiedFiles: modified,
3065
- readFiles: read,
3066
- deletedFiles: deleted,
3067
- errors,
3068
- decisions,
3069
- constraints,
3070
- topics,
3071
- timeline,
3072
- mediaAttachments,
3073
- mainGoal,
3074
- lastUserMessages,
3075
- lastErrors,
3076
- messageCount: msgs.length
3077
- };
3141
+ debug("prepareRun: profile=" + rc.profile + " model=" + rc.modelLabel);
3142
+ const out = rc;
3143
+ out.config = config;
3144
+ out.profileCfg = profileCfg;
3145
+ out.providerCaps = providerCaps;
3146
+ out.summaryAuth = { apiKey: auth.apiKey, headers: auth.headers };
3147
+ out.segAuth = { apiKey: segAuth.apiKey, headers: segAuth.headers };
3148
+ return advance(out, "_prepared");
3078
3149
  }
3079
3150
 
3151
+ // src/app/steps/window.ts
3152
+ init_tokens();
3153
+
3080
3154
  // src/infra/session-identity.ts
3081
3155
  import { randomUUID } from "crypto";
3082
3156
  var UNRESOLVED_PREFIX = "unresolved:";
@@ -3405,9 +3479,9 @@ function pruneRedundant(msgs, precomputedTcIdx) {
3405
3479
  if (msgs[i2].role !== "toolResult")
3406
3480
  continue;
3407
3481
  const tc = tcIdx.get(msgs[i2].toolCallId ?? "");
3408
- if (!tc || tc.name !== "read")
3482
+ if (!tc || classifyTool(tc.arguments) !== "accesses")
3409
3483
  continue;
3410
- const fp = tc?.arguments?.path ?? tc?.arguments?.file_path;
3484
+ const fp = extractToolPath(tc.arguments);
3411
3485
  if (!fp)
3412
3486
  continue;
3413
3487
  const arr = readIndices.get(fp) ?? [];
@@ -3486,7 +3560,8 @@ function pruneRedundant(msgs, precomputedTcIdx) {
3486
3560
  originalTokens += estimateTokens(text);
3487
3561
  if (!keep.has(idx))
3488
3562
  continue;
3489
- if (m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS) {
3563
+ const protectFromTruncation = m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && text.length < ERROR_SCAN_MAX_LEN && LIKELY_ERROR_RE.test(text);
3564
+ if (m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && !protectFromTruncation) {
3490
3565
  const head = text.slice(0, half);
3491
3566
  const tail = text.slice(-half);
3492
3567
  const truncated = head + `
@@ -3521,7 +3596,7 @@ init_logger();
3521
3596
  init_paths();
3522
3597
  init_fs();
3523
3598
  import path8 from "path";
3524
- import crypto6 from "crypto";
3599
+ import crypto5 from "crypto";
3525
3600
 
3526
3601
  // src/infra/git.ts
3527
3602
  init_logger();
@@ -3545,6 +3620,7 @@ function findGitRoot(cwd) {
3545
3620
  }
3546
3621
 
3547
3622
  // src/utils/fingerprint.ts
3623
+ init_constants();
3548
3624
  var LANG_MAP = {
3549
3625
  ".ts": "typescript",
3550
3626
  ".tsx": "typescript",
@@ -3584,7 +3660,7 @@ function isProjectPath(filePath) {
3584
3660
  return !NOISE_PATH_RE.test(filePath);
3585
3661
  }
3586
3662
  function hashProjectId(seed) {
3587
- return "proj-" + crypto6.createHash("sha256").update(seed).digest("hex").slice(0, 12);
3663
+ return ID_PREFIX.PROJECT + crypto5.createHash("sha256").update(seed).digest("hex").slice(0, TRUNC.PROJ_ID_HASH);
3588
3664
  }
3589
3665
  function findGitRoot2(cwd) {
3590
3666
  return findGitRoot(cwd);
@@ -3625,11 +3701,11 @@ function deriveFromRelativePaths(paths) {
3625
3701
  for (const p of paths) {
3626
3702
  const segs = p.split("/").filter(Boolean);
3627
3703
  if (segs.length >= 2) {
3628
- const d2 = segs.slice(0, 2).join("/");
3704
+ const d2 = segs.slice(0, TRUNC.FINGERPRINT_SEG).join("/");
3629
3705
  dir2Counts.set(d2, (dir2Counts.get(d2) ?? 0) + 1);
3630
3706
  }
3631
3707
  }
3632
- const stableDirs = [...dir2Counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([d]) => d).sort();
3708
+ const stableDirs = [...dir2Counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, TRUNC.CONV_HASH).map(([d]) => d).sort();
3633
3709
  return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
3634
3710
  }
3635
3711
  function deriveProjectId(cwd, extraction, sessionId) {
@@ -3697,7 +3773,7 @@ function loadProjectFingerprint(projectId) {
3697
3773
  const data = readJsonSync(getFingerprintPath(projectId));
3698
3774
  if (!data)
3699
3775
  return null;
3700
- if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
3776
+ if (Date.now() - data.updatedAt > THIRTY_DAYS_MS)
3701
3777
  return null;
3702
3778
  return data;
3703
3779
  }
@@ -3883,7 +3959,7 @@ function executeExplorationTool(call, llmMessages) {
3883
3959
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3884
3960
  idx: s + i,
3885
3961
  role: m?.role,
3886
- preview: extractText(m?.content).slice(0, 150),
3962
+ preview: extractText(m?.content).slice(0, TRUNC.PREVIEW),
3887
3963
  toolCalls: getToolCallNames(m?.content),
3888
3964
  isError: m?.isError
3889
3965
  })));
@@ -3906,7 +3982,7 @@ function executeExplorationTool(call, llmMessages) {
3906
3982
  return JSON.stringify(matches.map(({ idx, m }) => ({
3907
3983
  idx,
3908
3984
  role: m?.role,
3909
- preview: extractText(m?.content).slice(0, 150)
3985
+ preview: extractText(m?.content).slice(0, TRUNC.PREVIEW)
3910
3986
  })));
3911
3987
  }
3912
3988
  case "get_recent_user_messages": {
@@ -3919,7 +3995,7 @@ function executeExplorationTool(call, llmMessages) {
3919
3995
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3920
3996
  idx: s + i,
3921
3997
  role: m?.role,
3922
- text: extractText(m?.content).slice(0, 300),
3998
+ text: extractText(m?.content).slice(0, TRUNC.DETAIL),
3923
3999
  toolCalls: getToolCallNames(m?.content),
3924
4000
  isError: m?.isError
3925
4001
  })));
@@ -3931,22 +4007,16 @@ function executeExplorationTool(call, llmMessages) {
3931
4007
  const tcs = filterToolCalls(llmMessages[i]?.content);
3932
4008
  for (const block of tcs) {
3933
4009
  const a = block.arguments ?? {};
3934
- const fileFields = [
3935
- a.path,
3936
- a.file,
3937
- a.filePath,
3938
- a.file_path
3939
- ].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
4010
+ const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
3940
4011
  const matchesPath = fileFields.some((f) => f.includes(target));
3941
- if (block.name === "edit" && matchesPath) {
3942
- results.push({ idx: i, role: "assistant", toolCall: "edit", args: block.arguments, preview: extractText(llmMessages[i]?.content).slice(0, 400) });
3943
- }
3944
- if (block.name === "write" && matchesPath) {
3945
- results.push({ idx: i, role: "assistant", toolCall: "write", preview: extractText(llmMessages[i]?.content).slice(0, 400) });
4012
+ if (classifyTool(block.arguments) === "mutates" && matchesPath) {
4013
+ const surgical = a.oldText != null || a.newText != null || a.edits != null || a.patch != null;
4014
+ const preview = extractText(llmMessages[i]?.content).slice(0, TRUNC.PREVIEW_LONG);
4015
+ results.push(surgical ? { idx: i, role: "assistant", toolCall: block.name ?? "mutates", args: block.arguments, preview } : { idx: i, role: "assistant", toolCall: block.name ?? "mutates", preview });
3946
4016
  }
3947
4017
  }
3948
4018
  }
3949
- return JSON.stringify(results.slice(0, 15) || [{ info: "No edits found for: " + args.path }]);
4019
+ return JSON.stringify(results.slice(0, TRUNC.EXPLORE_RESULTS) || [{ info: "No edits found for: " + args.path }]);
3950
4020
  }
3951
4021
  case "get_error_chain": {
3952
4022
  const errIdx = args.index ?? 0;
@@ -3955,7 +4025,7 @@ function executeExplorationTool(call, llmMessages) {
3955
4025
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3956
4026
  idx: s + i,
3957
4027
  role: m?.role,
3958
- text: extractText(m?.content).slice(0, 500),
4028
+ text: extractText(m?.content).slice(0, TRUNC.PREVIEW_XL),
3959
4029
  isError: m?.isError,
3960
4030
  toolCalls: getToolCallNames(m?.content)
3961
4031
  })));
@@ -4009,7 +4079,7 @@ function normalizeBoundaries(raw, llmLength) {
4009
4079
  const confidence = b.confidence;
4010
4080
  return {
4011
4081
  afterIndex: Math.max(0, Math.min(b.afterIndex, maxIndex)),
4012
- topic: String(b.topic ?? "").slice(0, 100),
4082
+ topic: String(b.topic ?? "").slice(0, TRUNC.TOPIC_LABEL),
4013
4083
  priority: typeof priority === "string" && BOUNDARY_PRIORITIES.includes(priority) ? priority : "normal",
4014
4084
  confidence: typeof confidence === "number" && Number.isFinite(confidence) ? Math.min(1, Math.max(0, confidence)) : 0.5
4015
4085
  };
@@ -4057,12 +4127,12 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
4057
4127
  "Main goal: " + (extraction.mainGoal ?? "unknown"),
4058
4128
  "Files modified (" + extraction.modifiedFiles.length + "): " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none"),
4059
4129
  "Files read (" + extraction.readFiles.length + "): " + (extraction.readFiles.join(", ") || "none"),
4060
- "Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " (resolved)" : e.retryAttempted ? " (retry attempted)" : "")).join("; ") || "none"),
4061
- "Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 80)).join("; ") || "none"),
4062
- "Constraints (" + extraction.constraints.length + "): " + (extraction.constraints.map((cc) => "[" + cc.category + "] " + cc.text.slice(0, 80)).join("; ") || "none"),
4130
+ "Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, TRUNC.SNIPPET) + (e.resolved ? " (resolved)" : e.retryAttempted ? " (retry attempted)" : "")).join("; ") || "none"),
4131
+ "Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, TRUNC.SNIPPET)).join("; ") || "none"),
4132
+ "Constraints (" + extraction.constraints.length + "): " + (extraction.constraints.map((cc) => "[" + cc.category + "] " + cc.text.slice(0, TRUNC.SNIPPET)).join("; ") || "none"),
4063
4133
  "Heuristic topics (" + extraction.topics.length + "): " + (extraction.topics.map((t) => "[" + t.startIndex + "-" + t.endIndex + "] " + t.type).join("; ") || "none"),
4064
- extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0, 100)).join(" | ") : "",
4065
- extraction.lastErrors.length ? "Last errors: " + extraction.lastErrors.map((e) => e.slice(0, 100)).join(" | ") : ""
4134
+ extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0, TRUNC.TOPIC_LABEL)).join(" | ") : "",
4135
+ extraction.lastErrors.length ? "Last errors: " + extraction.lastErrors.map((e) => e.slice(0, TRUNC.TOPIC_LABEL)).join(" | ") : ""
4066
4136
  ].filter(Boolean).join(`
4067
4137
  `);
4068
4138
  const userContent = `Explore this conversation and produce the structured report.
@@ -4177,7 +4247,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
4177
4247
  return { report, rounds: 1, toolSupported: supportsTools };
4178
4248
  }
4179
4249
  async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal, services) {
4180
- const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
4250
+ const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.PREVIEW)).join(`
4181
4251
  `);
4182
4252
  const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
4183
4253
 
@@ -4203,10 +4273,10 @@ User steering: ` + userNote : "");
4203
4273
  }
4204
4274
  }
4205
4275
  async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal, services) {
4206
- const first3 = llmMessages.filter((m) => m?.role === "user").slice(0, 3).map((m) => extractText(m?.content).slice(0, 200)).join(`
4276
+ const first3 = llmMessages.filter((m) => m?.role === "user").slice(0, 3).map((m) => extractText(m?.content).slice(0, TRUNC.PREVIEW_MID)).join(`
4207
4277
  ---
4208
4278
  `);
4209
- const last30 = llmMessages.slice(-30).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 300)).join(`
4279
+ const last30 = llmMessages.slice(-30).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.DETAIL)).join(`
4210
4280
  `);
4211
4281
  const prompt = `Analyze this conversation and produce a JSON report.
4212
4282
 
@@ -4215,9 +4285,9 @@ First user messages:
4215
4285
 
4216
4286
  Deterministic data:
4217
4287
  ` + "- Files modified: " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none") + `
4218
- - Errors: ` + (extraction.errors.map((e) => e.message.slice(0, 80)).join("; ") || "none") + `
4219
- - Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0, 80)).join("; ") || "none") + `
4220
- - Constraints: ` + (extraction.constraints.map((c) => c.text.slice(0, 80)).join("; ") || "none") + `
4288
+ - Errors: ` + (extraction.errors.map((e) => e.message.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
4289
+ - Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
4290
+ - Constraints: ` + (extraction.constraints.map((c) => c.text.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
4221
4291
 
4222
4292
  Last 30 messages:
4223
4293
  ` + last30 + (prevSummary ? `
@@ -4330,7 +4400,7 @@ Session-specific instructions:
4330
4400
  async function summarizeBatch(batch, extraction, model, auth, signal, services) {
4331
4401
  const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
4332
4402
  const extractionCtx = buildExtractionContext(extraction, range);
4333
- const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0, 120) + (d.userResponse ? " \u2192 " + d.userResponse.slice(0, 60) : ""));
4403
+ const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0, TRUNC.OPEN_LOOP_SUMMARY) + (d.userResponse ? " \u2192 " + d.userResponse.slice(0, TRUNC.DECISION_DETAIL) : ""));
4334
4404
  const decisionCtx = activeDecisions.length ? `
4335
4405
  ## Active Decisions from previous segments (honour these):
4336
4406
  ` + activeDecisions.join(`
@@ -4340,8 +4410,8 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
4340
4410
  return "--- CHUNK " + id + ": " + ch.topic + " (" + ch.priority + `) ---
4341
4411
  ` + ch.messages.map((m) => {
4342
4412
  const role = m?.role ?? "unknown";
4343
- const content = extractText(m?.content).slice(0, 500);
4344
- const toolCalls = filterToolCalls(m?.content).map((tc) => tc.name + " " + JSON.stringify(tc.arguments).slice(0, 300)).join("; ");
4413
+ const content = extractText(m?.content).slice(0, TRUNC.PREVIEW_XL);
4414
+ const toolCalls = filterToolCalls(m?.content).map((tc) => tc.name + " " + JSON.stringify(tc.arguments).slice(0, TRUNC.DETAIL)).join("; ");
4345
4415
  const toolSuffix = toolCalls ? `
4346
4416
  [tool_calls] ` + toolCalls : "";
4347
4417
  return "[" + role + "] " + content + toolSuffix;
@@ -4383,9 +4453,9 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
4383
4453
  const prio = f("Priority").toLowerCase();
4384
4454
  const sectionFallback = sec.split(`
4385
4455
  `).slice(1).join(`
4386
- `).trim().slice(0, 500);
4387
- const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0, 180)).join(`
4388
- `).slice(0, 500);
4456
+ `).trim().slice(0, TRUNC.PREVIEW_XL);
4457
+ const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0, TRUNC.CHUNK_FALLBACK)).join(`
4458
+ `).slice(0, TRUNC.PREVIEW_XL);
4389
4459
  return {
4390
4460
  topic: ch.topic,
4391
4461
  startIndex: ch.startIndex,
@@ -4422,18 +4492,18 @@ function assembleFallback(summaries, extraction) {
4422
4492
  extraction.mainGoal ?? "See topics below.",
4423
4493
  "",
4424
4494
  "## Constraints & Preferences",
4425
- ...extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, 200)),
4495
+ ...extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, TRUNC.PREVIEW_MID)),
4426
4496
  "",
4427
4497
  "## Progress",
4428
4498
  "### Done",
4429
4499
  "- See topics below",
4430
4500
  "### In Progress",
4431
- ...summaries.filter((s) => s.priority === "high").map((s) => "- [ ] " + s.summary.slice(0, 150)),
4501
+ ...summaries.filter((s) => s.priority === "high").map((s) => "- [ ] " + s.summary.slice(0, TRUNC.PREVIEW)),
4432
4502
  "### Blocked",
4433
4503
  "- None",
4434
4504
  "",
4435
4505
  "## Key Decisions",
4436
- ...extraction.decisions.map((d) => "- **" + d.summary.slice(0, 100) + "**" + (d.userResponse ? " \u2192 " + d.userResponse : "")),
4506
+ ...extraction.decisions.map((d) => "- **" + d.summary.slice(0, TRUNC.TOPIC_LABEL) + "**" + (d.userResponse ? " \u2192 " + d.userResponse : "")),
4437
4507
  "",
4438
4508
  "## Files Modified",
4439
4509
  ...detModified.map((f) => "- " + f),
@@ -4445,10 +4515,10 @@ function assembleFallback(summaries, extraction) {
4445
4515
  "1. See topics below",
4446
4516
  "",
4447
4517
  "## Critical Context",
4448
- ...extraction.errors.filter((e) => !e.resolved).map((e) => "- Unresolved error: " + e.message.slice(0, 100)),
4518
+ ...extraction.errors.filter((e) => !e.resolved).map((e) => "- Unresolved error: " + e.message.slice(0, TRUNC.TOPIC_LABEL)),
4449
4519
  "",
4450
4520
  "## Topics Covered",
4451
- ...summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, 200))
4521
+ ...summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, TRUNC.PREVIEW_MID))
4452
4522
  ].join(`
4453
4523
  `);
4454
4524
  }
@@ -4458,7 +4528,7 @@ function failedChunkSummary(ch) {
4458
4528
  startIndex: ch.startIndex,
4459
4529
  endIndex: ch.endIndex,
4460
4530
  summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
4461
- `).slice(0, 300),
4531
+ `).slice(0, TRUNC.DETAIL),
4462
4532
  keyDecisions: [],
4463
4533
  filesModified: [],
4464
4534
  filesRead: [],
@@ -4860,6 +4930,16 @@ function appendToSection(summary, kind, text, fallbackBody = "") {
4860
4930
  ` : "") + text.trim());
4861
4931
  }
4862
4932
 
4933
+ // src/domain/keywords.ts
4934
+ function extractCheckKeywords(text, max) {
4935
+ const words = text.split(/\s+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter((w) => w.length > 3);
4936
+ if (!words.length)
4937
+ return [];
4938
+ const salient = (w) => /^[A-Z\u00C0-\u00DE]/.test(w) || /[0-9]/.test(w);
4939
+ const preferred = words.filter(salient);
4940
+ return (preferred.length ? preferred : words).slice(0, max);
4941
+ }
4942
+
4863
4943
  // src/phases/verify.ts
4864
4944
  function verifySummary(summary, extraction) {
4865
4945
  const parsed = parseSummary(summary);
@@ -4891,22 +4971,22 @@ function verifySummary(summary, extraction) {
4891
4971
  }
4892
4972
  }
4893
4973
  for (const e of extraction.errors.filter((e2) => !e2.resolved)) {
4894
- const snippet = e.message.slice(0, 30).toLowerCase();
4974
+ const snippet = e.message.trim().replace(/\s+/g, " ").slice(0, TRUNC.ERROR_SNIPPET).toLowerCase();
4895
4975
  if (snippet.length > 5 && !lower.includes(snippet)) {
4896
- gaps.push("Missing error: " + e.message.slice(0, 80));
4976
+ gaps.push("Missing error: " + e.message.slice(0, TRUNC.SNIPPET));
4897
4977
  score -= 5;
4898
4978
  }
4899
4979
  }
4900
4980
  for (const c of extraction.constraints.filter((c2) => c2.confidence >= 0.8)) {
4901
- const keywords = c.text.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
4981
+ const keywords = extractCheckKeywords(c.text, 3);
4902
4982
  const found = keywords.some((k) => lower.includes(k.toLowerCase()));
4903
4983
  if (!found && keywords.length > 0) {
4904
- gaps.push("Missing constraint: " + c.text.slice(0, 100));
4984
+ gaps.push("Missing constraint: " + c.text.slice(0, TRUNC.TOPIC_LABEL));
4905
4985
  score -= 3;
4906
4986
  }
4907
4987
  }
4908
4988
  if (extraction.mainGoal) {
4909
- const goalWords = extraction.mainGoal.split(/\s+/).filter((w) => w.length > 3).slice(0, 4);
4989
+ const goalWords = extractCheckKeywords(extraction.mainGoal, 4);
4910
4990
  const goalFound = goalWords.some((w) => lower.includes(w.toLowerCase()));
4911
4991
  if (!goalFound) {
4912
4992
  gaps.push("Main goal may be missing from summary");
@@ -4949,9 +5029,9 @@ function verifySummary(summary, extraction) {
4949
5029
  const decisionSection = findSection(parsed, "decisions");
4950
5030
  const decisionBody = decisionSection?.body.toLowerCase() ?? "";
4951
5031
  for (const d of highConfDecisions) {
4952
- const keywords = d.summary.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
5032
+ const keywords = extractCheckKeywords(d.summary, 3);
4953
5033
  if (keywords.length > 0 && !keywords.some((k) => decisionBody.includes(k.toLowerCase()))) {
4954
- gaps.push("Missing decision: " + d.summary.slice(0, 100));
5034
+ gaps.push("Missing decision: " + d.summary.slice(0, TRUNC.TOPIC_LABEL));
4955
5035
  score -= 3;
4956
5036
  }
4957
5037
  }
@@ -5130,7 +5210,7 @@ function loadCompactionState(projectId) {
5130
5210
  updatedAt = 0;
5131
5211
  }
5132
5212
  }
5133
- if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
5213
+ if (Date.now() - updatedAt > SEVEN_DAYS_MS)
5134
5214
  return null;
5135
5215
  }
5136
5216
  return data;
@@ -5139,21 +5219,18 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
5139
5219
  let decisionId = 0;
5140
5220
  let constraintId = 0;
5141
5221
  let errorId = 0;
5142
- const modifiedBasenames = extraction.modifiedFiles.map((f) => ({
5143
- path: f.path,
5144
- bn: f.path.split("/").pop()?.toLowerCase() ?? ""
5145
- }));
5222
+ const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
5146
5223
  return {
5147
5224
  goal: extraction.mainGoal,
5148
5225
  decisions: extraction.decisions.map((d) => ({
5149
- id: "decision-" + ++decisionId,
5150
- summary: d.summary.slice(0, 200),
5151
- ...d.userResponse ? { userResponse: d.userResponse.slice(0, 300) } : {},
5226
+ id: ID_PREFIX.DECISION + ++decisionId,
5227
+ summary: d.summary.slice(0, TRUNC.DECISION_SUMMARY),
5228
+ ...d.userResponse ? { userResponse: d.userResponse.slice(0, TRUNC.USER_RESPONSE) } : {},
5152
5229
  type: d.type
5153
5230
  })),
5154
5231
  constraints: extraction.constraints.map((c) => ({
5155
5232
  id: "constraint-" + ++constraintId,
5156
- text: c.text.slice(0, 300),
5233
+ text: c.text.slice(0, TRUNC.CONSTRAINT_TEXT),
5157
5234
  category: c.category,
5158
5235
  confidence: c.confidence
5159
5236
  })),
@@ -5162,17 +5239,17 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
5162
5239
  deletedFiles: extraction.deletedFiles,
5163
5240
  unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
5164
5241
  const msgLower = e.message.toLowerCase();
5165
- const bn = modifiedBasenames.find((f) => f.bn.length > 0 && msgLower.includes(f.bn));
5242
+ const match = fileNeedles.find(({ needles }) => needles.some((n) => msgLower.includes(n)));
5166
5243
  return {
5167
- id: "error-" + ++errorId,
5168
- message: e.message.slice(0, 300),
5244
+ id: ID_PREFIX.ERROR + ++errorId,
5245
+ message: e.message.slice(0, TRUNC.MESSAGE),
5169
5246
  tool: e.tool,
5170
- files: bn ? [bn.path] : []
5247
+ files: match ? [match.path] : []
5171
5248
  };
5172
5249
  }),
5173
5250
  resolvedErrors: extraction.errors.filter((e) => e.resolved).map((e) => ({
5174
- id: "error-" + ++errorId,
5175
- message: e.message.slice(0, 300),
5251
+ id: ID_PREFIX.ERROR + ++errorId,
5252
+ message: e.message.slice(0, TRUNC.MESSAGE),
5176
5253
  tool: e.tool
5177
5254
  })),
5178
5255
  openLoops,
@@ -5202,28 +5279,28 @@ function injectOpenLoopsSection(summary, openLoops) {
5202
5279
  return renderSummary(updated);
5203
5280
  }
5204
5281
  function computeDelta(prev, current) {
5205
- const prevDecisionTexts = new Set(prev.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
5206
- const currDecisionTexts = new Set(current.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
5207
- const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
5208
- const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
5209
- const prevLoopSummaries = new Map(prev.openLoops.map((l) => [l.summary.toLowerCase().slice(0, 50), l]));
5210
- const currLoopSummaries = new Map(current.openLoops.map((l) => [l.summary.toLowerCase().slice(0, 50), l]));
5282
+ const key = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
5283
+ const prevDecisionTexts = new Set(prev.decisions.map((d) => key(d.summary)));
5284
+ const currDecisionTexts = new Set(current.decisions.map((d) => key(d.summary)));
5285
+ const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(key(d.summary))).map((d) => d.summary);
5286
+ const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(key(d.summary))).map((d) => d.summary);
5287
+ const prevLoopSummaries = new Map(prev.openLoops.map((l) => [key(l.summary), l]));
5288
+ const currLoopKeys = new Set(current.openLoops.map((l) => key(l.summary)));
5211
5289
  const resolvedLoops = [];
5212
5290
  const persistentLoops = [];
5213
- for (const [key, loop] of prevLoopSummaries) {
5214
- if (currLoopSummaries.has(key)) {
5291
+ for (const [k, loop] of prevLoopSummaries) {
5292
+ if (currLoopKeys.has(k))
5215
5293
  persistentLoops.push(loop.summary);
5216
- } else {
5294
+ else
5217
5295
  resolvedLoops.push(loop.summary);
5218
- }
5219
5296
  }
5220
- const newLoops = current.openLoops.filter((l) => !prevLoopSummaries.has(l.summary.toLowerCase().slice(0, 50))).map((l) => l.summary);
5297
+ const newLoops = current.openLoops.filter((l) => !prevLoopSummaries.has(key(l.summary))).map((l) => l.summary);
5221
5298
  const prevFiles = new Set(prev.modifiedFiles);
5222
5299
  const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
5223
- const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
5224
- const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
5225
- const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(e.message.toLowerCase().slice(0, 40))).map((e) => e.message);
5226
- const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(e.message.toLowerCase().slice(0, 40))).map((e) => e.message);
5300
+ const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => key(e.message)));
5301
+ const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => key(e.message)));
5302
+ const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(key(e.message))).map((e) => e.message);
5303
+ const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(key(e.message))).map((e) => e.message);
5227
5304
  const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
5228
5305
  return {
5229
5306
  newDecisions,
@@ -5244,22 +5321,22 @@ function formatDeltaSection(delta) {
5244
5321
  lines.push("- **Goal shifted**: " + (delta.previousGoal ?? "?") + " \u2192 see current goal above");
5245
5322
  }
5246
5323
  if (delta.resolvedLoops.length) {
5247
- lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0, 60) + "~~").join(", "));
5324
+ lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0, TRUNC.DECISION_DETAIL) + "~~").join(", "));
5248
5325
  }
5249
5326
  if (delta.persistentLoops.length) {
5250
- lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0, 60)).join("; "));
5327
+ lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
5251
5328
  }
5252
5329
  if (delta.newLoops.length) {
5253
- lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0, 60)).join("; "));
5330
+ lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
5254
5331
  }
5255
5332
  if (delta.newDecisions.length) {
5256
- lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, 80)).join("; "));
5333
+ lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, TRUNC.SNIPPET)).join("; "));
5257
5334
  }
5258
5335
  if (delta.resolvedErrors.length) {
5259
- lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, 60)).join("; "));
5336
+ lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
5260
5337
  }
5261
5338
  if (delta.newErrors.length) {
5262
- lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0, 60)).join("; "));
5339
+ lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
5263
5340
  }
5264
5341
  if (delta.newModifiedFiles.length) {
5265
5342
  lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
@@ -5294,17 +5371,17 @@ function ensurePinnedPaths(summary, pinned) {
5294
5371
  return renderSummary(updated);
5295
5372
  }
5296
5373
  function extractNextActions(summary) {
5297
- const match = summary.match(/## Next Steps\s*\n([\s\S]*?)(?=##|$)/);
5298
- if (!match)
5374
+ const section = findSection(summary, "next-steps");
5375
+ if (!section)
5299
5376
  return [];
5300
- return match[1].split(`
5377
+ return section.body.split(`
5301
5378
  `).map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 0);
5302
5379
  }
5303
5380
  function extractCriticalContext(summary) {
5304
- const match = summary.match(/## Critical Context\s*\n([\s\S]*?)(?=##|$)/);
5305
- if (!match)
5381
+ const section = findSection(summary, "critical-context");
5382
+ if (!section)
5306
5383
  return [];
5307
- return match[1].split(`
5384
+ return section.body.split(`
5308
5385
  `).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
5309
5386
  }
5310
5387
 
@@ -5312,6 +5389,7 @@ function extractCriticalContext(summary) {
5312
5389
  init_logger();
5313
5390
  init_paths();
5314
5391
  init_fs();
5392
+ init_constants();
5315
5393
  var COMPLAINT_PATTERNS = [
5316
5394
  /(?:I already (?:told|said|mentioned|explained) you|(?:we|I) (?:already|just) (?:discussed|went over|covered) this|you forgot|you lost|nerede kald\u0131|hat\u0131rlam\u0131yor|unuttun)/i,
5317
5395
  /(?:that'?s? not (?:what I|right)|that'?s? wrong|yanl\u0131\u015F|hay\u0131r de\u011Fil|no that'|that doesn'?t match)/i,
@@ -5328,8 +5406,8 @@ function detectDamage(postMessages, details) {
5328
5406
  if (msg.role === "assistant") {
5329
5407
  const blocks = Array.isArray(msg.content) ? msg.content : [];
5330
5408
  for (const b of blocks) {
5331
- if (isToolCallBlock(b) && (b.name === "read" || b.name === "bash")) {
5332
- const fp = b.arguments?.path ?? b.arguments?.file_path;
5409
+ if (isToolCallBlock(b) && classifyTool(b.arguments) === "accesses") {
5410
+ const fp = extractToolPath(b.arguments);
5333
5411
  if (fp) {
5334
5412
  const fpLower = fp.toLowerCase();
5335
5413
  if (compactedFiles.has(fpLower) || compactedReadFiles.has(fpLower)) {
@@ -5351,18 +5429,18 @@ function detectDamage(postMessages, details) {
5351
5429
  signals.push({
5352
5430
  type: "user-complaint",
5353
5431
  severity: "high",
5354
- detail: 'User complaint after compaction: "' + text.slice(0, 100) + '"'
5432
+ detail: 'User complaint after compaction: "' + text.slice(0, TRUNC.TOPIC_LABEL) + '"'
5355
5433
  });
5356
5434
  break;
5357
5435
  }
5358
5436
  }
5359
5437
  for (const t of details.topics) {
5360
- const topicWords = t.toLowerCase().split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
5361
- if (topicWords.length >= 2 && topicWords.some((w) => text.includes(w))) {
5438
+ const topicWords = extractCheckKeywords(t, 3);
5439
+ if (topicWords.length > 0 && topicWords.some((w) => text.includes(w.toLowerCase()))) {
5362
5440
  signals.push({
5363
5441
  type: "re-question",
5364
5442
  severity: "low",
5365
- detail: "User mentions compacted topic: " + t.slice(0, 80)
5443
+ detail: "User mentions compacted topic: " + t.slice(0, TRUNC.SNIPPET)
5366
5444
  });
5367
5445
  }
5368
5446
  }
@@ -5412,7 +5490,7 @@ function logDamageReport(sessionId, report, details) {
5412
5490
  warn("logDamageReport failed", e);
5413
5491
  }
5414
5492
  }
5415
- var REMEDIATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
5493
+ var REMEDIATION_TTL_MS = SEVEN_DAYS_MS;
5416
5494
  function writeRemediationHints(projectId, files) {
5417
5495
  if (!files.length)
5418
5496
  return;
@@ -5854,34 +5932,29 @@ function unwrapConsumed(result, ctx) {
5854
5932
  return null;
5855
5933
  }
5856
5934
  }
5857
- function resolveModelArg(ctx, modelArg) {
5858
- const [p, ...r] = modelArg.split("/");
5935
+ function findModelById(ctx, modelId) {
5936
+ const [p, ...r] = modelId.split("/");
5859
5937
  return ctx.modelRegistry.find(p, r.join("/"));
5860
5938
  }
5861
- function resolveModels(ctx, primary, config) {
5939
+ function resolveModels(ctx, primary, config, explicit = false) {
5862
5940
  const fallback = primary ?? ctx.model;
5863
5941
  const available = ctx.modelRegistry.getAvailable();
5864
5942
  let sumModel = fallback;
5865
- const configuredSumModels = [config.summaryModel].filter(Boolean);
5866
- for (const modelId of configuredSumModels) {
5867
- const [p, ...r] = modelId.split("/");
5868
- const found = ctx.modelRegistry.find(p, r.join("/"));
5869
- if (found) {
5943
+ if (!explicit && config.summaryModel) {
5944
+ const found = findModelById(ctx, config.summaryModel);
5945
+ if (found)
5870
5946
  sumModel = found;
5871
- break;
5872
- }
5873
5947
  }
5874
5948
  if (sumModel === fallback && !fallback)
5875
5949
  sumModel = available[0];
5876
5950
  let segModel = sumModel;
5877
5951
  if (config.segmentationModel) {
5878
- const [p, ...r] = config.segmentationModel.split("/");
5879
- segModel = ctx.modelRegistry.find(p, r.join("/")) ?? sumModel;
5952
+ segModel = findModelById(ctx, config.segmentationModel) ?? sumModel;
5880
5953
  }
5881
5954
  return { segModel, sumModel };
5882
5955
  }
5883
5956
  function smartCompactExtension(pi) {
5884
- const PENDING_TTL_MS = 5 * 60 * 1000;
5957
+ const PENDING_TTL_MS = FIVE_MINUTES_MS;
5885
5958
  const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
5886
5959
  const isRunning = { value: false };
5887
5960
  pi.registerCommand("smart-compact", {
@@ -5978,7 +6051,7 @@ function smartCompactExtension(pi) {
5978
6051
  ctx.ui.notify("Cancelled", "info");
5979
6052
  return;
5980
6053
  }
5981
- const { segModel: segModel2, sumModel: sumModel2 } = resolveModels(ctx, selected.model.model, loadConfig());
6054
+ const { segModel: segModel2, sumModel: sumModel2 } = resolveModels(ctx, selected.model.model, loadConfig(), true);
5982
6055
  if (!sumModel2) {
5983
6056
  ctx.ui.notify("Could not resolve model", "error");
5984
6057
  return;
@@ -5986,7 +6059,7 @@ function smartCompactExtension(pi) {
5986
6059
  await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning, force: true });
5987
6060
  return;
5988
6061
  }
5989
- const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
6062
+ const { segModel, sumModel } = resolveModels(ctx, modelArg ? findModelById(ctx, modelArg) : ctx.model, loadConfig(), Boolean(modelArg));
5990
6063
  if (!sumModel) {
5991
6064
  ctx.ui.notify("Could not resolve model", "error");
5992
6065
  return;
@@ -6123,5 +6196,7 @@ Dashboard: ` + fp : "") }], details: undefined };
6123
6196
  });
6124
6197
  }
6125
6198
  export {
6199
+ resolveModels,
6200
+ findModelById,
6126
6201
  smartCompactExtension as default
6127
6202
  };