pi-smart-compact 7.15.1 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +166 -285
  3. package/SECURITY.md +14 -5
  4. package/SUPPORT.md +18 -5
  5. package/dist/app/run-smart-compact.d.ts.map +1 -1
  6. package/dist/app/steps/extract.d.ts.map +1 -1
  7. package/dist/app/steps/persist.d.ts.map +1 -1
  8. package/dist/app/steps/recover.d.ts.map +1 -1
  9. package/dist/app/steps/state.d.ts.map +1 -1
  10. package/dist/app/steps/synthesize.d.ts.map +1 -1
  11. package/dist/constants.d.ts +60 -4
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/domain/keywords.d.ts +20 -0
  14. package/dist/domain/keywords.d.ts.map +1 -0
  15. package/dist/domain/tool-semantics.d.ts +45 -0
  16. package/dist/domain/tool-semantics.d.ts.map +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1624 -1226
  19. package/dist/infra/ai-messages.d.ts +40 -0
  20. package/dist/infra/ai-messages.d.ts.map +1 -0
  21. package/dist/infra/paths.d.ts +2 -0
  22. package/dist/infra/paths.d.ts.map +1 -1
  23. package/dist/infra/services.d.ts +1 -0
  24. package/dist/infra/services.d.ts.map +1 -1
  25. package/dist/phases/explore.d.ts +1 -1
  26. package/dist/phases/explore.d.ts.map +1 -1
  27. package/dist/phases/synthesize.d.ts +7 -0
  28. package/dist/phases/synthesize.d.ts.map +1 -1
  29. package/dist/phases/verify.d.ts.map +1 -1
  30. package/dist/types.d.ts +3 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/ui/overlays.d.ts +7 -0
  33. package/dist/ui/overlays.d.ts.map +1 -1
  34. package/dist/utils/cache.d.ts.map +1 -1
  35. package/dist/utils/damage.d.ts +14 -0
  36. package/dist/utils/damage.d.ts.map +1 -1
  37. package/dist/utils/extraction.d.ts.map +1 -1
  38. package/dist/utils/fingerprint.d.ts.map +1 -1
  39. package/dist/utils/helpers.d.ts +33 -0
  40. package/dist/utils/helpers.d.ts.map +1 -1
  41. package/dist/utils/pruning.d.ts.map +1 -1
  42. package/dist/utils/session-log.d.ts.map +1 -1
  43. package/dist/utils/state.d.ts +7 -0
  44. package/dist/utils/state.d.ts.map +1 -1
  45. package/dist/utils/tokens.d.ts.map +1 -1
  46. package/docs/RELEASE.md +1 -1
  47. package/docs/assets/banner.svg +71 -0
  48. package/package.json +2 -2
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.15.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 = `
19
+ var VERSION = "7.18.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 = `
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 = {
@@ -77,10 +77,11 @@ var init_constants = __esm(() => {
77
77
  autoTriggerTimeoutMs: 120000,
78
78
  backupEnabled: true,
79
79
  backupDir: "",
80
- minContextPercent: 60
80
+ minContextPercent: 60,
81
+ pinPaths: []
81
82
  };
82
83
  NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
83
- 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;
84
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;
85
86
  SINGLE_PASS_PREFIX = `Summarize this coding agent conversation. Produce ONE structured summary.
86
87
  ` + `
@@ -177,6 +178,56 @@ Rules for Accuracy:
177
178
  discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
178
179
  };
179
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;
180
231
  EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
181
232
 
182
233
  ` + `Your job:
@@ -246,7 +297,7 @@ function damageReportsFile() {
246
297
  return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
247
298
  }
248
299
  function extractionCacheFile(sessionId) {
249
- 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");
250
301
  }
251
302
  function projectFingerprintFile(projectId) {
252
303
  return path.join(projectFingerprintDir(), projectId + ".json");
@@ -254,10 +305,15 @@ function projectFingerprintFile(projectId) {
254
305
  function compactionStateFile(projectId) {
255
306
  return path.join(compactionStateDir(), projectId + ".json");
256
307
  }
308
+ function remediationHintsFile(projectId) {
309
+ return path.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
310
+ }
257
311
  function metricsDashboardFile() {
258
312
  return path.join(cacheDir(), "smart-compact-report.html");
259
313
  }
260
- var init_paths = () => {};
314
+ var init_paths = __esm(() => {
315
+ init_constants();
316
+ });
261
317
 
262
318
  // src/infra/fs.ts
263
319
  import fs from "fs";
@@ -385,8 +441,8 @@ class TokenCalibrationStore {
385
441
  const key = calibrationKey(provider, model);
386
442
  const prev = this.factors.get(key) ?? 1;
387
443
  const sample = actual / estimated;
388
- const clamped = Math.max(0.3, Math.min(3, sample));
389
- 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);
390
446
  }
391
447
  }
392
448
  function calibrationKey(provider, model) {
@@ -394,7 +450,21 @@ function calibrationKey(provider, model) {
394
450
  }
395
451
  function estimateTokens(text, provider, model, calibration = _fallbackCalibration) {
396
452
  const baseRatio = provider ? getProviderCaps(provider).tokenRatioEstimate : CHARS_PER_TOKEN;
397
- 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
+ }
398
468
  const langPenalty = /[\u00E7\u011F\u0131\u00F6\u015F\u00FC\u00C7\u011E\u0130\u00D6\u015E\u00DC]/.test(text) ? 0.9 : 1;
399
469
  const factor = calibration.get(provider, model);
400
470
  return Math.ceil(text.length / baseRatio * jsonPenalty * langPenalty * factor);
@@ -402,7 +472,7 @@ function estimateTokens(text, provider, model, calibration = _fallbackCalibratio
402
472
  function calibrateFromResponse(estimated, actual, provider, model, calibration = _fallbackCalibration) {
403
473
  calibration.calibrate(estimated, actual, provider, model);
404
474
  }
405
- 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;
406
476
  var init_tokens = __esm(() => {
407
477
  init_constants();
408
478
  PROVIDER_MAP = {
@@ -740,7 +810,7 @@ import crypto4 from "crypto";
740
810
  class ToolSupportCache {
741
811
  entries = new Map;
742
812
  ttlMs;
743
- constructor(ttlMs = 60 * 60 * 1000) {
813
+ constructor(ttlMs = ONE_HOUR_MS) {
744
814
  this.ttlMs = ttlMs;
745
815
  }
746
816
  get(key, now) {
@@ -846,11 +916,7 @@ var init_services = __esm(() => {
846
916
 
847
917
  // src/utils/cache.ts
848
918
  import fs3 from "fs";
849
- import path4 from "path";
850
- import crypto5 from "crypto";
851
- function fallbackSessionId() {
852
- return "sc-" + Date.now().toString(36) + "-" + crypto5.randomBytes(4).toString("hex");
853
- }
919
+ import path5 from "path";
854
920
  function cacheOpts(opts, provider, phase, services) {
855
921
  if (phase && INTERNAL_PHASES.has(phase)) {
856
922
  return { ...opts, cacheRetention: "none" };
@@ -860,7 +926,7 @@ function cacheOpts(opts, provider, phase, services) {
860
926
  if (retention === "none") {
861
927
  return { ...opts, cacheRetention: "none" };
862
928
  }
863
- return { ...opts, sessionId: services?.compactSessionId ?? fallbackSessionId(), cacheRetention: retention };
929
+ return { ...opts, sessionId: services?.compactSessionId ?? makeCompactSessionId(), cacheRetention: retention };
864
930
  }
865
931
  function recordMetric(m, services = getDefaultServices()) {
866
932
  services.metrics.record(m);
@@ -966,14 +1032,14 @@ function scheduleExtractionCacheCleanup() {
966
1032
  _extractionPruneInFlight = true;
967
1033
  queueMicrotask(() => {
968
1034
  try {
969
- const dir = path4.dirname(getCachePath("_"));
1035
+ const dir = path5.dirname(getCachePath("_"));
970
1036
  if (!fs3.existsSync(dir))
971
1037
  return;
972
1038
  const now = Date.now();
973
1039
  for (const name of fs3.readdirSync(dir)) {
974
- if (!name.startsWith("compact-extraction-") || !name.endsWith(".json"))
1040
+ if (!name.startsWith(EXTRACTION_CACHE_PREFIX) || !name.endsWith(".json"))
975
1041
  continue;
976
- const fp = path4.join(dir, name);
1042
+ const fp = path5.join(dir, name);
977
1043
  try {
978
1044
  const stat = fs3.statSync(fp);
979
1045
  if (now - stat.mtimeMs > EXTRACTION_CACHE_PRUNE_MAX_AGE_MS) {
@@ -1019,9 +1085,9 @@ function mergeExtractions(base, delta, baseMsgCount) {
1019
1085
  constraints: [...base.constraints, ...offsetConstraints],
1020
1086
  topics: [...base.topics, ...offsetTopics],
1021
1087
  timeline: [...base.timeline, ...offsetTimeline],
1022
- mainGoal: delta.mainGoal ?? base.mainGoal,
1023
- lastUserMessages: delta.lastUserMessages.length > 0 ? delta.lastUserMessages : base.lastUserMessages,
1024
- 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),
1025
1091
  messageCount: baseMsgCount + delta.messageCount
1026
1092
  };
1027
1093
  }
@@ -1317,6 +1383,7 @@ var init_cache = __esm(() => {
1317
1383
  init_llm_client();
1318
1384
  init_paths();
1319
1385
  init_fs();
1386
+ init_constants();
1320
1387
  init_id_fingerprint();
1321
1388
  init_services();
1322
1389
  INTERNAL_PHASES = new Set([
@@ -1329,8 +1396,8 @@ var init_cache = __esm(() => {
1329
1396
  "assemble",
1330
1397
  "patch"
1331
1398
  ]);
1332
- EXTRACTION_CACHE_TTL_MS = 60 * 60 * 1000;
1333
- 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;
1334
1401
  });
1335
1402
 
1336
1403
  // src/ui/dashboard-format.ts
@@ -1434,9 +1501,12 @@ var DASHBOARD_PAGE_SIZE = 24;
1434
1501
  var exports_overlays = {};
1435
1502
  __export(exports_overlays, {
1436
1503
  showResultScreen: () => showResultScreen,
1504
+ showRestorePicker: () => showRestorePicker,
1505
+ showRestoreAction: () => showRestoreAction,
1437
1506
  showProgressOverlay: () => showProgressOverlay,
1438
1507
  showMetricsDashboardUI: () => showMetricsDashboardUI,
1439
1508
  showCompactUI: () => showCompactUI,
1509
+ showBackupViewer: () => showBackupViewer,
1440
1510
  selectProfile: () => selectProfile,
1441
1511
  selectModel: () => selectModel,
1442
1512
  renderTokenBar: () => renderTokenBar,
@@ -1444,7 +1514,7 @@ __export(exports_overlays, {
1444
1514
  });
1445
1515
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
1446
1516
  import { Container, Key, matchesKey, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
1447
- import path5 from "path";
1517
+ import path6 from "path";
1448
1518
  function renderContextBar(theme, pct, tokens, barLen = 24) {
1449
1519
  const clamped = Math.min(Math.max(pct, 0), 100);
1450
1520
  const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
@@ -1627,7 +1697,7 @@ async function showResultScreen(ctx, details, extraction, services) {
1627
1697
  const f = modFiles[i];
1628
1698
  const fc = extraction.modifiedFiles.find((e) => e.path === f);
1629
1699
  const count = fc ? " (" + fc.toolCalls + "x)" : "";
1630
- 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));
1631
1701
  }
1632
1702
  if (modFiles.length > maxShow) {
1633
1703
  c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
@@ -1650,7 +1720,7 @@ async function showResultScreen(ctx, details, extraction, services) {
1650
1720
  c.addChild(new Text(theme.fg("success", " \u2705 All facts verified \u2014 no gaps detected"), 0, 0));
1651
1721
  } else if (details.gaps.length > 0) {
1652
1722
  c.addChild(new Text(theme.fg("warning", " \u26A0\uFE0F " + details.gaps.length + " gap(s) patched:"), 0, 0));
1653
- for (const g of details.gaps.slice(0, 5)) {
1723
+ for (const g of details.gaps.slice(0, TRUNC.RESULT_GAPS)) {
1654
1724
  c.addChild(new Text(theme.fg("dim", " \u2022 " + g), 0, 0));
1655
1725
  }
1656
1726
  }
@@ -1797,7 +1867,123 @@ async function showCompactUI(ctx, opts) {
1797
1867
  return null;
1798
1868
  return { model: selectedModel, profile: selectedProfile };
1799
1869
  }
1870
+ async function showRestorePicker(ctx, backups) {
1871
+ const items = backups.map((b) => ({
1872
+ value: b.path,
1873
+ label: new Date(b.date).toLocaleString() + " \xB7 " + Math.max(1, Math.round(b.sizeBytes / 1024)) + "KB",
1874
+ description: b.sessionId.slice(0, TRUNC.SESSION_ID_DISPLAY)
1875
+ }));
1876
+ return await ctx.ui.custom((tui, theme, _kb, done) => {
1877
+ const c = new Container;
1878
+ c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
1879
+ c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Smart Compact \u2014 Restore")), 1, 0));
1880
+ c.addChild(new Text(theme.fg("dim", " Pick a backup to view its pre-compaction content"), 0, 0));
1881
+ c.addChild(new Text("", 0, 0));
1882
+ const sel = new SelectList(items, Math.min(items.length, 12), {
1883
+ selectedPrefix: (t) => theme.fg("accent", t),
1884
+ selectedText: (t) => theme.fg("accent", t),
1885
+ description: (t) => theme.fg("muted", t),
1886
+ scrollInfo: (t) => theme.fg("dim", t),
1887
+ noMatch: (t) => theme.fg("warning", t)
1888
+ });
1889
+ sel.onSelect = (item) => done(item.value);
1890
+ sel.onCancel = () => done(null);
1891
+ c.addChild(sel);
1892
+ c.addChild(new Text("", 0, 0));
1893
+ c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter view \xB7 esc cancel"), 0, 0));
1894
+ c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
1895
+ return {
1896
+ render: (w) => c.render(w),
1897
+ invalidate: () => c.invalidate(),
1898
+ handleInput: (d) => {
1899
+ sel.handleInput(d);
1900
+ tui.requestRender();
1901
+ }
1902
+ };
1903
+ });
1904
+ }
1905
+ async function showBackupViewer(ctx, content, fp) {
1906
+ await ctx.ui.custom((tui, theme, keybindings, done) => {
1907
+ const lines = content.split(`
1908
+ `);
1909
+ const pageSize = 40;
1910
+ let scroll = 0;
1911
+ const maxScroll = Math.max(0, lines.length - pageSize);
1912
+ return {
1913
+ render: (w) => {
1914
+ const out = [
1915
+ truncateToWidth(theme.fg("accent", theme.bold(" \u21A9 Restored backup")) + theme.fg("dim", " \xB7 " + lines.length + " lines \xB7 " + Math.max(1, Math.round(content.length / 1024)) + "KB"), w),
1916
+ truncateToWidth(theme.fg("dim", " " + fp), w),
1917
+ truncateToWidth(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, w))), w)
1918
+ ];
1919
+ for (const line of lines.slice(scroll, scroll + pageSize)) {
1920
+ out.push(truncateToWidth(theme.fg("text", line), w));
1921
+ }
1922
+ if (lines.length > pageSize) {
1923
+ out.push(truncateToWidth(theme.fg("dim", " showing " + (scroll + 1) + "\u2013" + Math.min(lines.length, scroll + pageSize) + " of " + lines.length), w));
1924
+ }
1925
+ out.push("", truncateToWidth(theme.fg("dim", " \u2191\u2193 scroll \xB7 pgup/pgdn \xB7 home/end \xB7 esc/q close"), w));
1926
+ return out;
1927
+ },
1928
+ invalidate: () => {},
1929
+ handleInput: (data) => {
1930
+ if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
1931
+ done(undefined);
1932
+ return;
1933
+ }
1934
+ if (matchesKey(data, Key.home))
1935
+ scroll = 0;
1936
+ else if (matchesKey(data, Key.end))
1937
+ scroll = maxScroll;
1938
+ else if (keybindings.matches(data, "tui.select.pageUp"))
1939
+ scroll = Math.max(0, scroll - pageSize);
1940
+ else if (keybindings.matches(data, "tui.select.pageDown"))
1941
+ scroll = Math.min(maxScroll, scroll + pageSize);
1942
+ else if (keybindings.matches(data, "tui.select.up"))
1943
+ scroll = Math.max(0, scroll - 1);
1944
+ else if (keybindings.matches(data, "tui.select.down"))
1945
+ scroll = Math.min(maxScroll, scroll + 1);
1946
+ tui.requestRender();
1947
+ }
1948
+ };
1949
+ }, { overlay: true, overlayOptions: { width: "85%", anchor: "center", maxHeight: "85%" } });
1950
+ }
1951
+ async function showRestoreAction(ctx, backupPath) {
1952
+ const items = [
1953
+ { value: "view", label: "View content", description: "Read the pre-compaction conversation" },
1954
+ { value: "restore", label: "Restore into a new session", description: "Fork from here + inject this backup as context" }
1955
+ ];
1956
+ return await ctx.ui.custom((tui, theme, _kb, done) => {
1957
+ const c = new Container;
1958
+ c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
1959
+ c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Restore action")), 1, 0));
1960
+ c.addChild(new Text(theme.fg("dim", " " + backupPath), 0, 0));
1961
+ c.addChild(new Text("", 0, 0));
1962
+ const sel = new SelectList(items, 2, {
1963
+ selectedPrefix: (t) => theme.fg("accent", t),
1964
+ selectedText: (t) => theme.fg("accent", t),
1965
+ description: (t) => theme.fg("muted", t),
1966
+ scrollInfo: (t) => theme.fg("dim", t),
1967
+ noMatch: (t) => theme.fg("warning", t)
1968
+ });
1969
+ sel.onSelect = (item) => done(item.value);
1970
+ sel.onCancel = () => done(null);
1971
+ c.addChild(sel);
1972
+ c.addChild(new Text("", 0, 0));
1973
+ c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter select \xB7 esc cancel"), 0, 0));
1974
+ c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
1975
+ return {
1976
+ render: (w) => c.render(w),
1977
+ invalidate: () => c.invalidate(),
1978
+ handleInput: (d) => {
1979
+ sel.handleInput(d);
1980
+ tui.requestRender();
1981
+ }
1982
+ };
1983
+ });
1984
+ }
1800
1985
  var init_overlays = __esm(() => {
1986
+ init_constants();
1801
1987
  init_cache();
1802
1988
  init_tokens();
1803
1989
  });
@@ -1810,1081 +1996,1161 @@ init_constants();
1810
1996
  init_logger();
1811
1997
  init_paths();
1812
1998
  init_fs();
1813
- init_constants();
1814
1999
  import fs2 from "fs";
1815
- import path3 from "path";
2000
+ import path4 from "path";
1816
2001
  import crypto2 from "crypto";
1817
- var VALID_PROFILES = ["light", "balanced", "aggressive"];
1818
- var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
1819
- function validateSmartCompactConfig(sc) {
1820
- if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
1821
- warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
1822
- delete sc.profile;
1823
- }
1824
- if ("autoTrigger" in sc && typeof sc.autoTrigger !== "boolean") {
1825
- warn("smart-compact config: autoTrigger must be boolean, got " + typeof sc.autoTrigger);
1826
- delete sc.autoTrigger;
1827
- }
1828
- if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
1829
- warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
1830
- delete sc.backupEnabled;
1831
- }
1832
- if ("summaryModel" in sc && sc.summaryModel !== null && typeof sc.summaryModel !== "string") {
1833
- warn("smart-compact config: summaryModel must be string|null, got " + typeof sc.summaryModel);
1834
- delete sc.summaryModel;
1835
- }
1836
- if ("segmentationModel" in sc && sc.segmentationModel !== null && typeof sc.segmentationModel !== "string") {
1837
- warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
1838
- delete sc.segmentationModel;
1839
- }
1840
- if ("profiles" in sc) {
1841
- if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
1842
- warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
1843
- delete sc.profiles;
1844
- } else {
1845
- const profiles = sc.profiles;
1846
- for (const [profileName, value] of Object.entries(profiles)) {
1847
- if (!VALID_PROFILES.includes(profileName)) {
1848
- warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
1849
- delete profiles[profileName];
1850
- continue;
1851
- }
1852
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1853
- warn("smart-compact config: profile '" + profileName + "' must be an object.");
1854
- delete profiles[profileName];
1855
- continue;
1856
- }
1857
- const profileCfg = value;
1858
- for (const [key, raw] of Object.entries(profileCfg)) {
1859
- if (!PROFILE_NUMERIC_KEYS.includes(key)) {
1860
- warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
1861
- delete profileCfg[key];
1862
- continue;
1863
- }
1864
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
1865
- warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
1866
- delete profileCfg[key];
1867
- }
1868
- }
1869
- }
1870
- }
1871
- }
1872
- if ("autoTriggerTimeoutMs" in sc) {
1873
- const v = sc.autoTriggerTimeoutMs;
1874
- if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
1875
- warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
1876
- delete sc.autoTriggerTimeoutMs;
1877
- }
1878
- }
1879
- if ("minContextPercent" in sc) {
1880
- const v = sc.minContextPercent;
1881
- if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
1882
- warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
1883
- delete sc.minContextPercent;
1884
- }
1885
- }
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";
1886
2011
  }
1887
- var _cfg = null;
1888
- var _cfgMtime = 0;
1889
- var _cfgPath = null;
1890
- function loadConfig() {
1891
- try {
1892
- const p = settingsFile();
1893
- const stat = fs2.statSync(p);
1894
- if (_cfg && _cfgPath === p && stat.mtimeMs === _cfgMtime)
1895
- return _cfg;
1896
- const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
1897
- const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
1898
- validateSmartCompactConfig(sc);
1899
- const merged = { ...DEFAULT_CONFIG, ...sc };
1900
- if (sc.profiles)
1901
- merged.profiles = { ...PROFILES, ...sc.profiles };
1902
- if (!merged.backupDir)
1903
- merged.backupDir = defaultBackupDir();
1904
- _cfg = merged;
1905
- _cfgMtime = stat.mtimeMs;
1906
- _cfgPath = p;
1907
- return _cfg;
1908
- } catch (e) {
1909
- debug("loadConfig: settings.json not found or unreadable, using defaults", e);
1910
- const fallback = { ...DEFAULT_CONFIG, backupDir: defaultBackupDir() };
1911
- _cfg = fallback;
1912
- _cfgPath = null;
1913
- return fallback;
1914
- }
2012
+ function isToolCallBlock(c) {
2013
+ return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
1915
2014
  }
1916
- var _pruneInFlight = new Set;
1917
- function prunePass(dir) {
1918
- try {
1919
- const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => {
1920
- const full = path3.join(dir, name);
1921
- try {
1922
- return { full, mtimeMs: fs2.statSync(full).mtimeMs };
1923
- } catch {
1924
- return null;
1925
- }
1926
- }).filter((v) => v !== null);
1927
- const now = Date.now();
1928
- const overAge = entries.filter((e) => now - e.mtimeMs > BACKUP_MAX_AGE_MS);
1929
- const sorted = entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
1930
- const overCount = sorted.slice(BACKUP_MAX_FILES);
1931
- const toRemove = new Set([...overAge, ...overCount].map((e) => e.full));
1932
- for (const full of toRemove) {
1933
- try {
1934
- fs2.unlinkSync(full);
1935
- } catch (e) {
1936
- debug("prunePass unlink failed", e);
1937
- }
1938
- }
1939
- } catch (e) {
1940
- debug("prunePass scan failed", e);
1941
- }
2015
+ function getToolCallNames(content) {
2016
+ if (!Array.isArray(content))
2017
+ return [];
2018
+ return content.filter(isToolCallBlock).map((b) => b.name);
1942
2019
  }
1943
- function schedulePruneBackups(dir) {
1944
- if (_pruneInFlight.has(dir))
1945
- return;
1946
- _pruneInFlight.add(dir);
1947
- queueMicrotask(() => {
1948
- try {
1949
- prunePass(dir);
1950
- } finally {
1951
- _pruneInFlight.delete(dir);
1952
- }
1953
- });
2020
+ function filterToolCalls(content) {
2021
+ if (!Array.isArray(content))
2022
+ return [];
2023
+ return content.filter(isToolCallBlock);
1954
2024
  }
1955
- function backupConversation(convText, sessionId) {
1956
- try {
1957
- const cfg = loadConfig();
1958
- if (!cfg.backupEnabled)
1959
- return null;
1960
- const dir = cfg.backupDir;
1961
- ensureDir(dir);
1962
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
1963
- const hash = crypto2.createHash("sha256").update(convText).digest("hex").slice(0, 8);
1964
- const fp = path3.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
1965
- atomicWriteFileSync(fp, `# Smart Compact Backup
1966
- # Date: ` + new Date().toISOString() + `
1967
- # Session: ` + sessionId + `
1968
-
1969
- ` + convText);
1970
- schedulePruneBackups(dir);
1971
- return fp;
1972
- } catch (e) {
1973
- warn("backupConversation failed", e);
1974
- return null;
1975
- }
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");
1976
2029
  }
1977
- function getPreviousCompactionContext(branch) {
1978
- const compactions = branch.filter((e) => e.type === "compaction");
1979
- if (!compactions.length)
1980
- return "";
1981
- const last = compactions[compactions.length - 1];
1982
- const topics = last.details?.topics ?? [];
1983
- if (!topics.length)
1984
- return "";
1985
- return `
1986
- [IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
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;
1987
2055
  }
1988
- function findLastAnchorIndex(branchEntries) {
1989
- for (let i = branchEntries.length - 1;i >= 0; i--) {
1990
- const e = branchEntries[i];
1991
- if (e?.type !== "message")
1992
- continue;
1993
- const msg = e.message;
1994
- if (msg?.role !== "toolResult")
1995
- continue;
1996
- if (msg?.toolName === "context" && msg?.details?.anchor) {
1997
- return i;
1998
- }
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;
2085
+ }
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);
1999
2111
  }
2000
- return -1;
2112
+ for (let j = parts.length - 2;j >= 0; j--) {
2113
+ needles.push(parts.slice(j).join("/"));
2114
+ }
2115
+ return needles;
2001
2116
  }
2002
- function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
2003
- let msgCount = 0;
2004
- for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
2005
- const e = branchEntries[i];
2006
- if (e?.type === "message") {
2007
- if (msgCount >= msgs.length)
2008
- return msgs.length - 1;
2009
- msgCount++;
2010
- }
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
+ });
2011
2170
  }
2012
- return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
2171
+ return [{ name: b.name, id: b.id, arguments: b.arguments }];
2013
2172
  }
2014
- function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
2015
- let adjusted = keepFromIndex;
2016
- if (branchEntries && branchEntries.length > 0) {
2017
- const lastAnchorBranchIdx = findLastAnchorIndex(branchEntries);
2018
- if (lastAnchorBranchIdx >= 0) {
2019
- const lastAnchorMsgIdx = branchIndexToMsgIndex(branchEntries, lastAnchorBranchIdx, msgs);
2020
- if (adjusted > lastAnchorMsgIdx && lastAnchorMsgIdx >= 0) {
2021
- 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 });
2022
2216
  }
2023
2217
  }
2024
2218
  }
2025
- if (adjusted <= 0 || adjusted >= msgs.length)
2026
- return adjusted;
2027
- const last = msgs[adjusted - 1];
2028
- const first = msgs[adjusted];
2029
- if (last && first) {
2030
- const getText = (msg) => {
2031
- const m = msg;
2032
- const c = m?.content;
2033
- if (typeof c === "string")
2034
- return c;
2035
- if (Array.isArray(c))
2036
- return c.map((b) => {
2037
- if (typeof b === "string")
2038
- return b;
2039
- if (typeof b === "object" && b !== null && b.type === "text")
2040
- return b.text ?? "";
2041
- return "";
2042
- }).join("");
2043
- return "";
2044
- };
2045
- const lastText = getText(last.message).toLowerCase();
2046
- const keptText = getText(first.message).toLowerCase();
2047
- const fileRe = /(?:path|file)=["']([^"']+)["']/g;
2048
- const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
2049
- fileRe.lastIndex = 0;
2050
- const keptFiles = new Set([...keptText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
2051
- if ([...lastFiles].filter((f) => keptFiles.has(f)).length > 0)
2052
- return adjusted - 1;
2053
- }
2054
- return adjusted;
2219
+ return out;
2055
2220
  }
2056
- function collectToolCallIds(blocks, msgIndex, out) {
2057
- for (const b of blocks) {
2058
- const block = b;
2059
- if (block?.type === "toolCall") {
2060
- if (typeof block.id === "string") {
2061
- 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 });
2062
2233
  }
2063
- const args = block.arguments;
2064
- if (block.name === "multi_tool_use.parallel" && args && Array.isArray(args.tool_uses)) {
2065
- for (const nested of args.tool_uses) {
2066
- const n = nested;
2067
- if (typeof n.id === "string") {
2068
- out.set(n.id, msgIndex);
2069
- }
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 });
2070
2240
  }
2071
2241
  }
2072
2242
  }
2073
2243
  }
2244
+ return idx;
2074
2245
  }
2075
- function guardToolCallBoundary(msgs, keepFrom) {
2076
- if (keepFrom <= 0 || keepFrom >= msgs.length)
2077
- return keepFrom;
2078
- 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;
2079
2251
  for (let i = 0;i < msgs.length; i++) {
2080
- const m = msgs[i].message;
2081
- if (m?.role !== "assistant")
2252
+ const m = msgs[i];
2253
+ if (m.role !== "toolResult" || m.isError)
2082
2254
  continue;
2083
- const blocks = Array.isArray(m?.content) ? m.content : [];
2084
- collectToolCallIds(blocks, i, tcMap);
2085
- }
2086
- let adjusted = keepFrom;
2087
- let changed = true;
2088
- const MAX_ITER = msgs.length + 1;
2089
- let iter = 0;
2090
- while (changed) {
2091
- if (++iter > MAX_ITER) {
2092
- warn("guardToolCallBoundary hit MAX_ITER=" + MAX_ITER + " at adjusted=" + adjusted);
2093
- break;
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);
2094
2273
  }
2095
- changed = false;
2096
- for (let i = adjusted;i < msgs.length; i++) {
2097
- const m = msgs[i].message;
2098
- if (m?.role !== "toolResult")
2274
+ }
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;
2292
+ }
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")
2099
2303
  continue;
2100
- const tcId = m?.toolCallId;
2101
- 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)
2102
2308
  continue;
2103
- const tcIdx = tcMap.get(tcId);
2104
- if (tcIdx !== undefined && tcIdx < adjusted) {
2105
- adjusted = tcIdx;
2106
- changed = true;
2107
- 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
+ }
2108
2319
  }
2320
+ break;
2109
2321
  }
2110
2322
  }
2111
- return Math.max(0, Math.min(adjusted, msgs.length));
2112
- }
2113
- function extractUserNote(args) {
2114
- const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
2115
- const tokens = args.trim().split(/\s+/).filter(Boolean);
2116
- const isOptionToken = (t) => t.startsWith("--") || SKIP.has(t.toLowerCase()) || /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t);
2117
- const nonFlags = tokens.filter((t) => !isOptionToken(t));
2118
- return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
2323
+ return errors;
2119
2324
  }
2120
- function createBatches(chunks, maxTokens) {
2121
- const batches = [];
2122
- let batch = [], bt = 0;
2123
- for (const ch of chunks) {
2124
- if (batch.length && bt + ch.tokenEstimate > maxTokens) {
2125
- batches.push(batch);
2126
- batch = [];
2127
- 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
+ }
2128
2340
  }
2129
- batch.push(ch);
2130
- bt += ch.tokenEstimate;
2131
2341
  }
2132
- if (batch.length)
2133
- batches.push(batch);
2134
- return batches;
2135
- }
2136
- function allocateTopicBudgets(summaries, totalBudget) {
2137
- const n = summaries.length;
2138
- if (n === 0)
2139
- return new Map;
2140
- const weights = summaries.map((s, i) => {
2141
- let w = 1;
2142
- if (s.priority === "critical")
2143
- w *= 2;
2144
- else if (s.priority === "high")
2145
- w *= 1.5;
2146
- else if (s.priority === "low")
2147
- w *= 0.6;
2148
- const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
2149
- w *= 1 + errorKeywords * 0.2;
2150
- const recency = (i + 1) / n;
2151
- w *= 0.6 + recency * 0.4;
2152
- if (s.keyDecisions.length > 0)
2153
- w *= 1.3;
2154
- return w;
2155
- });
2156
- const totalWeight = weights.reduce((a, b) => a + b, 0);
2157
- const baseTokensPerTopic = Math.floor(totalBudget / n);
2158
- const budgetMap = new Map;
2159
- for (let i = 0;i < summaries.length; i++) {
2160
- const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
2161
- 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
+ }
2162
2349
  }
2163
- return budgetMap;
2164
- }
2165
- function preProcessSummaries(summaries, budgetTokens) {
2166
- const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
2167
- return {
2168
- decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
2169
- modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
2170
- read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
2171
- text: summaries.map((cs, i) => {
2172
- const budgetHint = topicBudgets?.get(cs.topic);
2173
- const budgetLine = budgetHint ? `
2174
- Budget: ~` + budgetHint + " tokens" : "";
2175
- return "### Segment " + (i + 1) + ": " + cs.topic + `
2176
- Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
2177
-
2178
- ` + cs.summary + `
2179
-
2180
- Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
2181
- Modified: ` + (cs.filesModified.join(", ") || "None") + `
2182
- Read: ` + (cs.filesRead.join(", ") || "None");
2183
- }).join(`
2184
- ---
2185
- `)
2186
- };
2187
- }
2188
- function buildExtractionContext(extraction, forRange) {
2189
- const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
2190
- const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
2191
- const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
2192
- return [
2193
- "## Deterministic Extraction (verified facts)",
2194
- "Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
2195
- "Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
2196
- "Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 60)).join("; ") || "none"),
2197
- "Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none"),
2198
- "Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
2199
- ].join(`
2200
- `);
2350
+ return decisions;
2201
2351
  }
2202
- function computeToolCharPercentage(branchEntries) {
2203
- let totalChars = 0;
2204
- let toolChars = 0;
2205
- for (const raw of branchEntries) {
2206
- const m = raw?.message;
2207
- 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")
2208
2364
  continue;
2209
- let mc = 0;
2210
- if (typeof m.content === "string") {
2211
- mc = m.content.length;
2212
- } else if (Array.isArray(m.content)) {
2213
- for (const part of m.content) {
2214
- if (!part || typeof part !== "object")
2215
- continue;
2216
- const block = part;
2217
- if (block.type === "text" && typeof block.text === "string")
2218
- mc += block.text.length;
2219
- else if (block.type === "text" && typeof block.content === "string")
2220
- mc += block.content.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;
2221
2372
  }
2222
2373
  }
2223
- totalChars += mc;
2224
- if (m.role === "toolResult")
2225
- toolChars += mc;
2226
2374
  }
2227
- return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
2375
+ return constraints;
2228
2376
  }
2229
- function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
2230
- if (totalTokens < minThreshold)
2231
- return "none";
2232
- if (contextPercent < minContextPercent)
2233
- return "none";
2234
- if (contextPercent < 80)
2235
- return "light";
2236
- 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;
2237
2442
  }
2238
- function inferSessionType(extraction, report) {
2239
- if (report?.sessionType)
2240
- return report.sessionType;
2241
- const hasModifications = extraction.modifiedFiles.length > 0;
2242
- const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
2243
- const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
2244
- const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
2245
- const hasDecisions = extraction.decisions.length > 0;
2246
- if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
2247
- return "debugging";
2248
- if (hasReadsOnly && !hasDecisions)
2249
- return "review";
2250
- if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
2251
- return "discussion";
2252
- if (hasModifications)
2253
- return "implementation";
2254
- return "implementation";
2255
- }
2256
- function buildExplorationContext(report) {
2257
- if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
2258
- return "";
2259
- return [
2260
- "## Exploration Report",
2261
- "Main goal: " + report.mainGoal,
2262
- "Session type: " + report.sessionType,
2263
- report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
2264
- report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
2265
- report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
2266
- report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
2267
- report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
2268
- ].filter(Boolean).join(`
2269
- `);
2270
- }
2271
-
2272
- // src/index.ts
2273
- init_tokens();
2274
- init_cache();
2275
-
2276
- // src/app/run-smart-compact.ts
2277
- init_overlays();
2278
- init_logger();
2279
-
2280
- // src/app/run-context.ts
2281
- function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
2282
- rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
2283
- rc.phaseStart = endMs;
2284
- }
2285
- function markPhase(rc, phase) {
2286
- 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;
2287
2457
  }
2288
- function advance(rc, _stage) {
2289
- 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;
2290
2467
  }
2291
-
2292
- // src/app/run-smart-compact.ts
2293
- init_services();
2294
-
2295
- // src/app/steps/prepare.ts
2296
- init_constants();
2297
- init_tokens();
2298
- init_logger();
2299
- async function prepareRun(rc) {
2300
- const config = loadConfig();
2301
- const profileCfg = { ...PROFILES[rc.profile], ...config.profiles?.[rc.profile] ?? {} };
2302
- const providerCaps = getProviderCaps(rc.summaryModel.provider);
2303
- const auth = await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.summaryModel);
2304
- const segAuth = rc.segModel !== rc.summaryModel ? await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.segModel) : auth;
2305
- if (!auth.ok || !auth.apiKey || (!segAuth.ok || !segAuth.apiKey)) {
2306
- if (!rc.flags.autoTriggered)
2307
- rc.ctx.ui.notify("Auth failed", "error");
2308
- 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
+ });
2309
2487
  }
2310
- if (rc.timeoutMs > 0) {
2311
- rc.cancellation.timeoutId = setTimeout(() => {
2312
- rc.cancellation.timedOut = true;
2313
- rc.cancellation.controller.abort();
2314
- rc.notify("Smart compact auto-trigger exceeded " + rc.timeoutMs + "ms; Pi will use native compact for this run", "warning");
2315
- }, 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
+ }
2316
2510
  }
2317
- debug("prepareRun: profile=" + rc.profile + " model=" + rc.modelLabel);
2318
- const out = rc;
2319
- out.config = config;
2320
- out.profileCfg = profileCfg;
2321
- out.providerCaps = providerCaps;
2322
- out.summaryAuth = { apiKey: auth.apiKey, headers: auth.headers };
2323
- out.segAuth = { apiKey: segAuth.apiKey, headers: segAuth.headers };
2324
- return advance(out, "_prepared");
2325
- }
2326
-
2327
- // src/app/steps/window.ts
2328
- init_tokens();
2329
-
2330
- // src/utils/extraction.ts
2331
- init_constants();
2332
- init_tokens();
2333
- import path6 from "path";
2334
-
2335
- // src/utils/type-guards.ts
2336
- function isTextBlock(c) {
2337
- return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
2338
- }
2339
- function isToolCallBlock(c) {
2340
- return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
2341
- }
2342
- function getToolCallNames(content) {
2343
- if (!Array.isArray(content))
2344
- return [];
2345
- return content.filter(isToolCallBlock).map((b) => b.name);
2346
- }
2347
- function filterToolCalls(content) {
2348
- if (!Array.isArray(content))
2349
- return [];
2350
- return content.filter(isToolCallBlock);
2351
- }
2352
- var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
2353
- var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
2354
- function isStringArray(v) {
2355
- return Array.isArray(v) && v.every((x) => typeof x === "string");
2356
- }
2357
- function isValidSmartCompactDetails(d) {
2358
- if (!d || typeof d !== "object")
2359
- return false;
2360
- const r = d;
2361
- if (!isStringArray(r.modifiedFiles))
2362
- return false;
2363
- if (!isStringArray(r.readFiles))
2364
- return false;
2365
- if (!isStringArray(r.topics))
2366
- return false;
2367
- if (typeof r.method !== "string" || !KNOWN_METHODS.has(r.method))
2368
- return false;
2369
- if (typeof r.profile !== "string" || !KNOWN_PROFILES.has(r.profile))
2370
- return false;
2371
- if (typeof r.qualityScore !== "number" || !Number.isFinite(r.qualityScore))
2372
- return false;
2373
- if (typeof r.totalMessages !== "number" || !Number.isFinite(r.totalMessages))
2374
- return false;
2375
- if (r.gaps !== undefined && !isStringArray(r.gaps))
2376
- return false;
2377
- if (r.verified !== undefined && typeof r.verified !== "boolean")
2378
- return false;
2379
- if (r.backupPath !== undefined && r.backupPath !== null && typeof r.backupPath !== "string")
2380
- return false;
2381
- 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;
2382
2547
  }
2383
- function sanitizeSmartCompactDetails(d) {
2384
- if (isValidSmartCompactDetails(d))
2385
- return d;
2386
- if (!d || typeof d !== "object")
2387
- return null;
2388
- const r = d;
2389
- if (!isStringArray(r.modifiedFiles) || !isStringArray(r.readFiles) || !isStringArray(r.topics))
2390
- return null;
2391
- const repaired = {
2392
- method: KNOWN_METHODS.has(r.method) ? r.method : "heuristic",
2393
- chunkCount: typeof r.chunkCount === "number" ? r.chunkCount : 0,
2394
- topics: r.topics,
2395
- readFiles: r.readFiles,
2396
- modifiedFiles: r.modifiedFiles,
2397
- totalMessages: typeof r.totalMessages === "number" ? r.totalMessages : 0,
2398
- totalTokensSummarized: typeof r.totalTokensSummarized === "number" ? r.totalTokensSummarized : 0,
2399
- llmCalls: typeof r.llmCalls === "number" ? r.llmCalls : 0,
2400
- profile: KNOWN_PROFILES.has(r.profile) ? r.profile : "balanced",
2401
- backupPath: typeof r.backupPath === "string" ? r.backupPath : null,
2402
- tokensSaved: typeof r.tokensSaved === "number" ? r.tokensSaved : 0,
2403
- verified: typeof r.verified === "boolean" ? r.verified : false,
2404
- gaps: isStringArray(r.gaps) ? r.gaps : [],
2405
- explorationRounds: typeof r.explorationRounds === "number" ? r.explorationRounds : 0,
2406
- explorationBoundaries: typeof r.explorationBoundaries === "number" ? r.explorationBoundaries : 0,
2407
- model: typeof r.model === "string" ? r.model : "unknown",
2408
- qualityScore: typeof r.qualityScore === "number" ? r.qualityScore : 0,
2409
- 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
2410
2574
  };
2411
- return isValidSmartCompactDetails(repaired) ? repaired : null;
2412
2575
  }
2413
2576
 
2414
- // src/utils/file-needles.ts
2415
- var GENERIC_BASENAMES = new Set([
2416
- "index.ts",
2417
- "index.js",
2418
- "index.tsx",
2419
- "index.jsx",
2420
- "types.ts",
2421
- "helpers.ts",
2422
- "utils.ts",
2423
- "main.ts",
2424
- "main.js",
2425
- "mod.rs",
2426
- "lib.rs",
2427
- "__init__.py"
2428
- ]);
2429
- var MIN_BARE_BASENAME_LEN = 5;
2430
- function buildPathNeedles(filePath) {
2431
- const parts = filePath.toLowerCase().split("/").filter(Boolean);
2432
- if (parts.length === 0)
2433
- return [];
2434
- const needles = [];
2435
- const basename = parts[parts.length - 1];
2436
- if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
2437
- 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;
2438
2585
  }
2439
- for (let j = parts.length - 2;j >= 0; j--) {
2440
- 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;
2441
2589
  }
2442
- return needles;
2443
- }
2444
-
2445
- // src/utils/extraction.ts
2446
- var TRUNCATE_RE = /\u2026\u2702\d+$/;
2447
- function isTruncated(content) {
2448
- return TRUNCATE_RE.test(extractText(content));
2449
- }
2450
- var WRITE_TOOL_HINTS = ["write", "edit", "patch", "create", "append", "update", "apply"];
2451
- var DELETE_TOOL_HINTS = ["delete", "remove", "unlink"];
2452
- var READ_TOOL_HINTS = ["read", "view", "open"];
2453
- function hasToolHint(tool, hints) {
2454
- return hints.some((hint) => tool.includes(hint));
2455
- }
2456
- function flattenToolCallBlock(b) {
2457
- if (!isToolCallBlock(b))
2458
- return [];
2459
- if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2460
- return b.arguments.tool_uses.map((u) => {
2461
- const recipient = u?.recipient_name ?? "";
2462
- return {
2463
- name: recipient.replace(/^functions\./, ""),
2464
- id: u?.id ?? undefined,
2465
- arguments: u?.parameters ?? {}
2466
- };
2467
- });
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
+ }
2468
2657
  }
2469
- return [{ name: b.name, id: b.id, arguments: b.arguments }];
2470
- }
2471
- function extractText(content) {
2472
- if (typeof content === "string")
2473
- return content;
2474
- if (!Array.isArray(content))
2475
- return "";
2476
- return content.map((b) => {
2477
- if (typeof b === "string")
2478
- return b;
2479
- if (isTextBlock(b))
2480
- return b.text;
2481
- return "";
2482
- }).join("");
2483
2658
  }
2484
- function mediaKind(type, mime) {
2485
- const s = (type + " " + (mime ?? "")).toLowerCase();
2486
- if (/image|input_image|image_url/.test(s))
2487
- return "image";
2488
- if (/audio/.test(s))
2489
- return "audio";
2490
- if (/video/.test(s))
2491
- return "video";
2492
- if (/file|document|pdf|attachment/.test(s))
2493
- return "file";
2494
- return "unknown";
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;
2686
+ }
2495
2687
  }
2496
- function extractMediaAttachments(msgs) {
2497
- const out = [];
2498
- for (let i = 0;i < msgs.length; i++) {
2499
- const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
2500
- for (const b of blocks) {
2501
- if (!b || typeof b !== "object")
2502
- continue;
2503
- const rec = b;
2504
- const type = String(rec.type ?? "");
2505
- if (type === "text" || type === "toolCall" || type === "tool_use")
2506
- continue;
2507
- const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
2508
- const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
2509
- const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
2510
- const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
2511
- const kind = mediaKind(type, mimeType);
2512
- if (kind !== "unknown" || source || mimeType || name) {
2513
- 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);
2514
2709
  }
2515
2710
  }
2711
+ } catch (e) {
2712
+ debug("prunePass scan failed", e);
2516
2713
  }
2517
- return out;
2518
2714
  }
2519
- function buildToolCallIndex(msgs) {
2520
- const idx = new Map;
2521
- for (let i = 0;i < msgs.length; i++) {
2522
- const m = msgs[i];
2523
- if (m.role !== "assistant")
2524
- continue;
2525
- const blocks = Array.isArray(m.content) ? m.content : [];
2526
- for (const b of blocks) {
2527
- if (!isToolCallBlock(b))
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);
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;
2747
+ }
2748
+ }
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"))
2528
2757
  continue;
2529
- if (b.id) {
2530
- idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
2531
- }
2532
- if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2533
- const nested = flattenToolCallBlock(b);
2534
- for (let t = 0;t < nested.length; t++) {
2535
- const tool = nested[t];
2536
- const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
2537
- idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
2538
- }
2539
- }
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 {}
2540
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 [];
2541
2781
  }
2542
- return idx;
2543
2782
  }
2544
- function trackFileOps(msgs, _tcIdx) {
2545
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2546
- const modMap = new Map;
2547
- const readSet = new Set;
2548
- const delSet = new Set;
2549
- for (let i = 0;i < msgs.length; i++) {
2550
- const m = msgs[i];
2551
- if (m.role !== "toolResult" || m.isError)
2552
- continue;
2553
- const tc = tcIdx.get(m.toolCallId ?? "");
2554
- if (!tc)
2555
- continue;
2556
- const args = tc.arguments;
2557
- const filePath = args?.path ?? args?.file_path ?? args?.filePath;
2558
- if (!filePath)
2559
- continue;
2560
- const tool = tc.name.toLowerCase();
2561
- if (hasToolHint(tool, WRITE_TOOL_HINTS)) {
2562
- const resultText = extractText(m.content);
2563
- if (isTruncated(resultText)) {
2564
- const existing = modMap.get(filePath);
2565
- modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
2566
- } else if (!NO_OP_RE.test(resultText)) {
2567
- const existing = modMap.get(filePath);
2568
- modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
2569
- }
2570
- } else if (hasToolHint(tool, DELETE_TOOL_HINTS)) {
2571
- delSet.add(filePath);
2572
- } else if (hasToolHint(tool, READ_TOOL_HINTS)) {
2573
- readSet.add(filePath);
2574
- }
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;
2575
2798
  }
2799
+ }
2800
+ function buildRestoreMessage(content, source) {
2576
2801
  return {
2577
- modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
2578
- read: [...readSet],
2579
- 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() }
2580
2809
  };
2581
2810
  }
2582
- function catalogErrors(msgs, _tcIdx) {
2583
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2584
- const errors = [];
2585
- for (let i = 0;i < msgs.length; i++) {
2586
- const m = msgs[i];
2587
- 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")
2588
2826
  continue;
2589
- const tc = tcIdx.get(m.toolCallId ?? "");
2590
- if (m.isError) {
2591
- 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")
2592
2829
  continue;
2593
- }
2594
- if (tc?.name === "bash") {
2595
- const txt = extractText(m.content);
2596
- 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);
2597
- if (isLikelyError && txt.length < 2000) {
2598
- errors.push({ index: i, tool: "bash", message: txt.slice(0, 300), retryAttempted: false, resolved: false });
2599
- }
2830
+ if (msg?.toolName === "context" && msg?.details?.anchor) {
2831
+ return i;
2600
2832
  }
2601
2833
  }
2602
- for (const err of errors) {
2603
- for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
2604
- if (msgs[j]?.role === "assistant") {
2605
- const rawBlocks = msgs[j]?.content;
2606
- const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
2607
- for (const b of blocks) {
2608
- for (const tool of flattenToolCallBlock(b)) {
2609
- if (tool.name === err.tool) {
2610
- err.retryAttempted = true;
2611
- for (let k = j + 1;k < Math.min(msgs.length, j + 10); k++) {
2612
- if (msgs[k]?.role === "toolResult" && msgs[k]?.toolCallId === tool.id && !msgs[k]?.isError) {
2613
- err.resolved = true;
2614
- break;
2615
- }
2616
- }
2617
- break;
2618
- }
2619
- }
2620
- if (err.retryAttempted)
2621
- break;
2622
- }
2623
- if (err.retryAttempted)
2624
- break;
2625
- }
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++;
2626
2844
  }
2627
2845
  }
2628
- return errors;
2846
+ return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
2629
2847
  }
2630
- function extractDecisions(msgs, _tcIdx) {
2631
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2632
- const decisions = [];
2633
- for (const [id, tc] of tcIdx) {
2634
- if (tc.name !== "ask_user")
2635
- continue;
2636
- const args = tc.arguments;
2637
- const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
2638
- if (!question)
2639
- continue;
2640
- for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
2641
- if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
2642
- decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, 200), userResponse: extractText(msgs[i].content).slice(0, 300) });
2643
- 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;
2644
2856
  }
2645
2857
  }
2646
2858
  }
2647
- for (let i = 0;i < msgs.length; i++) {
2648
- if (msgs[i]?.role !== "user")
2649
- continue;
2650
- const txt = extractText(msgs[i].content);
2651
- if (CHOICE_RE.test(txt)) {
2652
- decisions.push({ index: i, type: "implicit", summary: txt.slice(0, 200) });
2653
- }
2654
- }
2655
- return decisions;
2656
- }
2657
- var CONSTRAINT_PATTERNS = [
2658
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: 0.85 },
2659
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: 0.8 },
2660
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: 0.6 },
2661
- { 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 },
2662
- { re: /\b(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))\b/iu, cat: "prohibition", conf: 0.8 },
2663
- { re: /\b(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)\b/iu, cat: "preference", conf: 0.6 }
2664
- ];
2665
- function mineConstraints(msgs) {
2666
- const constraints = [];
2667
- for (let i = 0;i < msgs.length; i++) {
2668
- if (msgs[i]?.role !== "user")
2669
- continue;
2670
- const txt = extractText(msgs[i].content);
2671
- if (txt.length < 10 || txt.startsWith("/"))
2672
- continue;
2673
- for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
2674
- if (re.test(txt)) {
2675
- constraints.push({ index: i, text: txt.slice(0, 300), category: cat, confidence: conf });
2676
- 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);
2677
2870
  }
2678
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;
2679
2879
  }
2680
- return constraints;
2880
+ return adjusted;
2681
2881
  }
2682
- function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2683
- const topics = [];
2684
- let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
2685
- let currentType = "exploration";
2686
- let currentPrimaryFile = null;
2687
- const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
2688
- for (let i = 0;i < msgs.length; i++) {
2689
- const m = msgs[i];
2690
- const txt = extractText(m.content);
2691
- tokenAcc += estimateTokens(txt);
2692
- let brk = false;
2693
- let type = "exploration";
2694
- let primaryFile = null;
2695
- if (m.role === "assistant") {
2696
- const blocks = Array.isArray(m.content) ? m.content : [];
2697
- for (const b of blocks) {
2698
- for (const tool of flattenToolCallBlock(b)) {
2699
- const fp = tool.arguments?.path ?? tool.arguments?.file_path;
2700
- if (!fp)
2701
- continue;
2702
- const fn = path6.basename(fp);
2703
- if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
2704
- brk = true;
2705
- lastFile = fn;
2706
- primaryFile = fp;
2707
- currentPrimaryFile = fp;
2708
- if (tool.name?.includes("write") || tool.name?.includes("edit")) {
2709
- type = "implementation";
2710
- if (currentType !== "implementation")
2711
- currentType = "implementation";
2712
- } else if (tool.name?.includes("read")) {
2713
- type = "review";
2714
- if (currentType === "exploration")
2715
- 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);
2716
2895
  }
2717
2896
  }
2718
2897
  }
2719
2898
  }
2720
- if (m.role === "toolResult" && m.isError) {
2721
- errAcc++;
2722
- type = "debugging";
2723
- if (currentType !== "implementation")
2724
- currentType = "debugging";
2725
- }
2726
- if (m.role === "toolResult" && !m.isError) {
2727
- const tc = tcIdx.get(m.toolCallId ?? "");
2728
- if (tc?.name === "bash" && /error|fail/i.test(txt)) {
2729
- errAcc++;
2730
- type = "debugging";
2731
- if (currentType !== "implementation")
2732
- currentType = "debugging";
2733
- }
2734
- }
2735
- if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
2736
- brk = true;
2737
- if (tokenAcc >= pc.maxChunkTokens)
2738
- brk = true;
2739
- if (brk && i > startIdx && topics.length < maxSegs - 1) {
2740
- topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2741
- startIdx = i + 1;
2742
- tokenAcc = 0;
2743
- lastFile = null;
2744
- errAcc = 0;
2745
- currentType = "exploration";
2746
- currentPrimaryFile = null;
2747
- }
2748
- }
2749
- if (startIdx < msgs.length) {
2750
- topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2751
2899
  }
2752
- return topics;
2753
2900
  }
2754
- function buildTimeline(msgs, errors) {
2755
- const timeline = [];
2756
- const errorIndices = new Set(errors.map((e) => e.index));
2901
+ function guardToolCallBoundary(msgs, keepFrom) {
2902
+ if (keepFrom <= 0 || keepFrom >= msgs.length)
2903
+ return keepFrom;
2904
+ const tcMap = new Map;
2757
2905
  for (let i = 0;i < msgs.length; i++) {
2758
- const m = msgs[i];
2759
- if (m.role === "user") {
2760
- const txt = extractText(m.content);
2761
- if (!txt.startsWith("/"))
2762
- timeline.push({ index: i, event: "user_request", summary: txt.slice(0, 150) });
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;
2920
+ }
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;
2934
+ }
2763
2935
  }
2764
- if (errorIndices.has(i))
2765
- timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, 100) ?? "error" });
2766
2936
  }
2767
- return timeline.length > 30 ? [...timeline.filter((t) => t.event === "user_request").slice(0, 10), ...timeline.filter((t) => t.event === "error")] : timeline;
2937
+ return Math.max(0, Math.min(adjusted, msgs.length));
2768
2938
  }
2769
- function extractMainGoal(msgs) {
2770
- for (const m of msgs) {
2771
- if (m?.role !== "user")
2772
- continue;
2773
- const txt = extractText(m.content).trim();
2774
- if (txt && !txt.startsWith("/"))
2775
- return txt.slice(0, 300);
2776
- }
2777
- return null;
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;
2778
2945
  }
2779
- function extractOpenLoops(msgs, extraction) {
2780
- const loops = [];
2781
- let loopId = 0;
2782
- const fileNeedles = extraction.modifiedFiles.map((f) => ({
2783
- path: f.path,
2784
- needles: buildPathNeedles(f.path)
2785
- }));
2786
- for (const err of extraction.errors.filter((e) => !e.resolved)) {
2787
- const errLower = err.message.toLowerCase();
2788
- const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path7 }) => path7);
2789
- loops.push({
2790
- id: "loop-" + ++loopId,
2791
- type: "bugfix",
2792
- priority: err.retryAttempted ? "high" : "normal",
2793
- status: "open",
2794
- summary: err.message.slice(0, 120),
2795
- files: errFiles,
2796
- sourceIndex: err.index
2797
- });
2798
- }
2799
- 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;
2800
- for (let idx = 0;idx < msgs.length; idx++) {
2801
- const msg = msgs[idx];
2802
- if (msg.role !== "user")
2803
- continue;
2804
- const txt = extractText(msg.content);
2805
- if (txt.length < 10 || txt.startsWith("/"))
2806
- continue;
2807
- if (FOLLOWUP_RE.test(txt)) {
2808
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2809
- if (!isDup) {
2810
- loops.push({
2811
- id: "loop-" + ++loopId,
2812
- type: "follow-up",
2813
- priority: "normal",
2814
- status: "open",
2815
- summary: txt.slice(0, 120),
2816
- files: [],
2817
- sourceIndex: idx
2818
- });
2819
- }
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;
2820
2954
  }
2955
+ batch.push(ch);
2956
+ bt += ch.tokenEstimate;
2821
2957
  }
2822
- const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
2823
- for (let idx = 0;idx < msgs.length; idx++) {
2824
- const msg = msgs[idx];
2825
- if (msg.role !== "user")
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));
2988
+ }
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
+ };
3013
+ }
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
+ `);
3027
+ }
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)
2826
3034
  continue;
2827
- const txt = extractText(msg.content);
2828
- if (BLOCKED_RE.test(txt)) {
2829
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2830
- if (!isDup) {
2831
- loops.push({
2832
- id: "loop-" + ++loopId,
2833
- type: "blocked",
2834
- priority: "high",
2835
- status: "open",
2836
- summary: txt.slice(0, 120),
2837
- files: [],
2838
- sourceIndex: idx
2839
- });
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;
2840
3045
  }
2841
3046
  }
3047
+ totalChars += mc;
3048
+ if (m.role === "toolResult")
3049
+ toolChars += mc;
2842
3050
  }
2843
- for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
2844
- const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
2845
- if (!exists) {
2846
- loops.push({
2847
- id: "loop-" + ++loopId,
2848
- type: "retry",
2849
- priority: "high",
2850
- status: "open",
2851
- summary: "Retried but unresolved: " + err.message.slice(0, 80),
2852
- files: [],
2853
- sourceIndex: err.index
2854
- });
2855
- }
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;
2856
3133
  }
2857
- return loops;
2858
- }
2859
- function extractStructured(msgs, pc, precomputedTcIdx) {
2860
- const tcIdx = precomputedTcIdx ?? buildToolCallIndex(msgs);
2861
- const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
2862
- const errors = catalogErrors(msgs, tcIdx);
2863
- const decisions = extractDecisions(msgs, tcIdx);
2864
- const constraints = mineConstraints(msgs);
2865
- const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
2866
- const timeline = buildTimeline(msgs, errors);
2867
- const mediaAttachments = extractMediaAttachments(msgs);
2868
- const mainGoal = extractMainGoal(msgs);
2869
- const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
2870
- const lastErrors = errors.slice(-3).map((e) => e.message);
2871
- return {
2872
- modifiedFiles: modified,
2873
- readFiles: read,
2874
- deletedFiles: deleted,
2875
- errors,
2876
- decisions,
2877
- constraints,
2878
- topics,
2879
- timeline,
2880
- mediaAttachments,
2881
- mainGoal,
2882
- lastUserMessages,
2883
- lastErrors,
2884
- messageCount: msgs.length
2885
- };
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);
3140
+ }
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");
2886
3149
  }
2887
3150
 
3151
+ // src/app/steps/window.ts
3152
+ init_tokens();
3153
+
2888
3154
  // src/infra/session-identity.ts
2889
3155
  import { randomUUID } from "crypto";
2890
3156
  var UNRESOLVED_PREFIX = "unresolved:";
@@ -2947,6 +3213,14 @@ function resolveCompactionWindow(rc) {
2947
3213
  // src/app/steps/recover.ts
2948
3214
  import { convertToLlm } from "@earendil-works/pi-coding-agent";
2949
3215
 
3216
+ // src/infra/ai-messages.ts
3217
+ function asBranchMessage(message) {
3218
+ return message;
3219
+ }
3220
+ function asSerializableMessages(msgs) {
3221
+ return msgs;
3222
+ }
3223
+
2950
3224
  // src/utils/session-log.ts
2951
3225
  import * as fs4 from "fs";
2952
3226
  import * as path7 from "path";
@@ -3059,17 +3333,19 @@ function findSessionLogFile(sessionId) {
3059
3333
  }
3060
3334
  return remember(null);
3061
3335
  }
3062
- function normalizeLogMessage(msg) {
3336
+ function normalizeLogMessage(msg, entryTimestamp) {
3063
3337
  if (!msg || !msg.role)
3064
3338
  return null;
3065
3339
  const role = msg.role;
3066
3340
  if (role === "user" || role === "assistant" || role === "toolResult") {
3341
+ const ts = entryTimestamp ? Date.parse(entryTimestamp) : NaN;
3067
3342
  return {
3068
3343
  role,
3069
3344
  content: msg.content,
3070
3345
  isError: msg.isError,
3071
3346
  toolCallId: msg.toolCallId,
3072
- timestamp: msg.content && typeof msg.content === "object" ? Date.now() : undefined
3347
+ toolName: msg.toolName,
3348
+ timestamp: Number.isFinite(ts) ? ts : undefined
3073
3349
  };
3074
3350
  }
3075
3351
  return null;
@@ -3100,7 +3376,7 @@ function readOriginalMessageMap(sessionId) {
3100
3376
  continue;
3101
3377
  }
3102
3378
  if (entry.type === "message" && entry.id && entry.message) {
3103
- const normalized = normalizeLogMessage(entry.message);
3379
+ const normalized = normalizeLogMessage(entry.message, entry.timestamp);
3104
3380
  if (normalized)
3105
3381
  map.set(entry.id, normalized);
3106
3382
  }
@@ -3148,7 +3424,7 @@ function resolveCompactionMessages(sessionId, toCompactEntries) {
3148
3424
 
3149
3425
  // src/app/steps/recover.ts
3150
3426
  function recoverSessionLog(rc) {
3151
- let llmMessages = convertToLlm(rc.toCompact.map((e) => e.message));
3427
+ let llmMessages = convertToLlm(rc.toCompact.map((e) => asBranchMessage(e.message)));
3152
3428
  if (hasTruncatedMessages(llmMessages)) {
3153
3429
  const fromLog = resolveCompactionMessages(rc.sessionId, rc.toCompact);
3154
3430
  if (fromLog) {
@@ -3203,9 +3479,9 @@ function pruneRedundant(msgs, precomputedTcIdx) {
3203
3479
  if (msgs[i2].role !== "toolResult")
3204
3480
  continue;
3205
3481
  const tc = tcIdx.get(msgs[i2].toolCallId ?? "");
3206
- if (!tc || tc.name !== "read")
3482
+ if (!tc || classifyTool(tc.arguments) !== "accesses")
3207
3483
  continue;
3208
- const fp = tc?.arguments?.path ?? tc?.arguments?.file_path;
3484
+ const fp = extractToolPath(tc.arguments);
3209
3485
  if (!fp)
3210
3486
  continue;
3211
3487
  const arr = readIndices.get(fp) ?? [];
@@ -3284,7 +3560,8 @@ function pruneRedundant(msgs, precomputedTcIdx) {
3284
3560
  originalTokens += estimateTokens(text);
3285
3561
  if (!keep.has(idx))
3286
3562
  continue;
3287
- 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) {
3288
3565
  const head = text.slice(0, half);
3289
3566
  const tail = text.slice(-half);
3290
3567
  const truncated = head + `
@@ -3319,7 +3596,7 @@ init_logger();
3319
3596
  init_paths();
3320
3597
  init_fs();
3321
3598
  import path8 from "path";
3322
- import crypto6 from "crypto";
3599
+ import crypto5 from "crypto";
3323
3600
 
3324
3601
  // src/infra/git.ts
3325
3602
  init_logger();
@@ -3343,6 +3620,7 @@ function findGitRoot(cwd) {
3343
3620
  }
3344
3621
 
3345
3622
  // src/utils/fingerprint.ts
3623
+ init_constants();
3346
3624
  var LANG_MAP = {
3347
3625
  ".ts": "typescript",
3348
3626
  ".tsx": "typescript",
@@ -3382,7 +3660,7 @@ function isProjectPath(filePath) {
3382
3660
  return !NOISE_PATH_RE.test(filePath);
3383
3661
  }
3384
3662
  function hashProjectId(seed) {
3385
- 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);
3386
3664
  }
3387
3665
  function findGitRoot2(cwd) {
3388
3666
  return findGitRoot(cwd);
@@ -3423,11 +3701,11 @@ function deriveFromRelativePaths(paths) {
3423
3701
  for (const p of paths) {
3424
3702
  const segs = p.split("/").filter(Boolean);
3425
3703
  if (segs.length >= 2) {
3426
- const d2 = segs.slice(0, 2).join("/");
3704
+ const d2 = segs.slice(0, TRUNC.FINGERPRINT_SEG).join("/");
3427
3705
  dir2Counts.set(d2, (dir2Counts.get(d2) ?? 0) + 1);
3428
3706
  }
3429
3707
  }
3430
- 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();
3431
3709
  return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
3432
3710
  }
3433
3711
  function deriveProjectId(cwd, extraction, sessionId) {
@@ -3495,7 +3773,7 @@ function loadProjectFingerprint(projectId) {
3495
3773
  const data = readJsonSync(getFingerprintPath(projectId));
3496
3774
  if (!data)
3497
3775
  return null;
3498
- if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
3776
+ if (Date.now() - data.updatedAt > THIRTY_DAYS_MS)
3499
3777
  return null;
3500
3778
  return data;
3501
3779
  }
@@ -3551,7 +3829,7 @@ function extractWithCache(rc) {
3551
3829
  const pruneEnd = Date.now();
3552
3830
  markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
3553
3831
  const extractionStart = pruneEnd;
3554
- const convText = serializeConversation(rc.llmMessages);
3832
+ const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
3555
3833
  const convTokens = estimateTokens(convText);
3556
3834
  const backupPath = backupConversation(convText, rc.sessionId);
3557
3835
  const prevContext = getPreviousCompactionContext(rc.branch);
@@ -3622,6 +3900,7 @@ function extractWithCache(rc) {
3622
3900
  init_overlays();
3623
3901
 
3624
3902
  // src/phases/explore.ts
3903
+ import { Type } from "typebox";
3625
3904
  init_constants();
3626
3905
  init_cache();
3627
3906
  init_tokens();
@@ -3644,32 +3923,32 @@ var EXPLORATION_TOOLS = [
3644
3923
  {
3645
3924
  name: "get_message_range",
3646
3925
  description: "Get compact summaries of messages from start to end index (0-based).",
3647
- parameters: { type: "object", properties: { start: { type: "number" }, end: { type: "number" } }, required: ["start", "end"] }
3926
+ parameters: Type.Object({ start: Type.Number(), end: Type.Number() })
3648
3927
  },
3649
3928
  {
3650
3929
  name: "search_conversation",
3651
3930
  description: "Search for text in conversation messages.",
3652
- parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }
3931
+ parameters: Type.Object({ query: Type.String() })
3653
3932
  },
3654
3933
  {
3655
3934
  name: "get_recent_user_messages",
3656
3935
  description: "Get the last N user messages.",
3657
- parameters: { type: "object", properties: { count: { type: "number" } } }
3936
+ parameters: Type.Object({ count: Type.Optional(Type.Number()) })
3658
3937
  },
3659
3938
  {
3660
3939
  name: "get_context_around",
3661
3940
  description: "Get context around a specific message index.",
3662
- parameters: { type: "object", properties: { index: { type: "number" }, radius: { type: "number" } }, required: ["index"] }
3941
+ parameters: Type.Object({ index: Type.Number(), radius: Type.Optional(Type.Number()) })
3663
3942
  },
3664
3943
  {
3665
3944
  name: "get_file_changes",
3666
3945
  description: "Get tool calls that modified a specific file.",
3667
- parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
3946
+ parameters: Type.Object({ path: Type.String() })
3668
3947
  },
3669
3948
  {
3670
3949
  name: "get_error_chain",
3671
3950
  description: "Get all messages related to a specific error.",
3672
- parameters: { type: "object", properties: { index: { type: "number" }, context_radius: { type: "number" } }, required: ["index"] }
3951
+ parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
3673
3952
  }
3674
3953
  ];
3675
3954
  function executeExplorationTool(call, llmMessages) {
@@ -3680,7 +3959,7 @@ function executeExplorationTool(call, llmMessages) {
3680
3959
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3681
3960
  idx: s + i,
3682
3961
  role: m?.role,
3683
- preview: extractText(m?.content).slice(0, 150),
3962
+ preview: extractText(m?.content).slice(0, TRUNC.PREVIEW),
3684
3963
  toolCalls: getToolCallNames(m?.content),
3685
3964
  isError: m?.isError
3686
3965
  })));
@@ -3703,7 +3982,7 @@ function executeExplorationTool(call, llmMessages) {
3703
3982
  return JSON.stringify(matches.map(({ idx, m }) => ({
3704
3983
  idx,
3705
3984
  role: m?.role,
3706
- preview: extractText(m?.content).slice(0, 150)
3985
+ preview: extractText(m?.content).slice(0, TRUNC.PREVIEW)
3707
3986
  })));
3708
3987
  }
3709
3988
  case "get_recent_user_messages": {
@@ -3716,7 +3995,7 @@ function executeExplorationTool(call, llmMessages) {
3716
3995
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3717
3996
  idx: s + i,
3718
3997
  role: m?.role,
3719
- text: extractText(m?.content).slice(0, 300),
3998
+ text: extractText(m?.content).slice(0, TRUNC.DETAIL),
3720
3999
  toolCalls: getToolCallNames(m?.content),
3721
4000
  isError: m?.isError
3722
4001
  })));
@@ -3728,22 +4007,16 @@ function executeExplorationTool(call, llmMessages) {
3728
4007
  const tcs = filterToolCalls(llmMessages[i]?.content);
3729
4008
  for (const block of tcs) {
3730
4009
  const a = block.arguments ?? {};
3731
- const fileFields = [
3732
- a.path,
3733
- a.file,
3734
- a.filePath,
3735
- a.file_path
3736
- ].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());
3737
4011
  const matchesPath = fileFields.some((f) => f.includes(target));
3738
- if (block.name === "edit" && matchesPath) {
3739
- results.push({ idx: i, role: "assistant", toolCall: "edit", args: block.arguments, preview: extractText(llmMessages[i]?.content).slice(0, 400) });
3740
- }
3741
- if (block.name === "write" && matchesPath) {
3742
- 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 });
3743
4016
  }
3744
4017
  }
3745
4018
  }
3746
- 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 }]);
3747
4020
  }
3748
4021
  case "get_error_chain": {
3749
4022
  const errIdx = args.index ?? 0;
@@ -3752,7 +4025,7 @@ function executeExplorationTool(call, llmMessages) {
3752
4025
  return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
3753
4026
  idx: s + i,
3754
4027
  role: m?.role,
3755
- text: extractText(m?.content).slice(0, 500),
4028
+ text: extractText(m?.content).slice(0, TRUNC.PREVIEW_XL),
3756
4029
  isError: m?.isError,
3757
4030
  toolCalls: getToolCallNames(m?.content)
3758
4031
  })));
@@ -3785,40 +4058,53 @@ function parseExplorationReport(text, llmMessages) {
3785
4058
  if (boundaryMatch) {
3786
4059
  try {
3787
4060
  const boundaries = JSON.parse("[" + boundaryMatch[1] + "]");
3788
- return { ...fallbackExplorationReport(llmMessages), boundaries: boundaries.filter((b) => typeof b?.afterIndex === "number").map((b) => ({
3789
- afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
3790
- topic: String(b.topic ?? "").slice(0, 100),
3791
- priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
3792
- confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
3793
- })) };
4061
+ return { ...fallbackExplorationReport(llmMessages), boundaries: normalizeBoundaries(boundaries, llmMessages.length) };
3794
4062
  } catch (err) {
3795
4063
  debug("Boundary JSON parse failed", err);
3796
4064
  }
3797
4065
  }
3798
4066
  return fallbackExplorationReport(llmMessages);
3799
4067
  }
4068
+ var BOUNDARY_PRIORITIES = ["critical", "high", "normal", "low"];
4069
+ var SESSION_TYPES = ["implementation", "review", "debugging", "discussion"];
4070
+ function stringArray(v) {
4071
+ return Array.isArray(v) ? v.map(String) : [];
4072
+ }
4073
+ function normalizeBoundaries(raw, llmLength) {
4074
+ if (!Array.isArray(raw))
4075
+ return [];
4076
+ const maxIndex = Math.max(0, llmLength - 2);
4077
+ return raw.filter((b) => !!b && typeof b === "object" && typeof b.afterIndex === "number").map((b) => {
4078
+ const priority = b.priority;
4079
+ const confidence = b.confidence;
4080
+ return {
4081
+ afterIndex: Math.max(0, Math.min(b.afterIndex, maxIndex)),
4082
+ topic: String(b.topic ?? "").slice(0, TRUNC.TOPIC_LABEL),
4083
+ priority: typeof priority === "string" && BOUNDARY_PRIORITIES.includes(priority) ? priority : "normal",
4084
+ confidence: typeof confidence === "number" && Number.isFinite(confidence) ? Math.min(1, Math.max(0, confidence)) : 0.5
4085
+ };
4086
+ });
4087
+ }
3800
4088
  function buildExplorationReportFromParsed(parsed, llmMessages) {
3801
4089
  if (typeof parsed !== "object" || parsed === null) {
3802
4090
  return fallbackExplorationReport(llmMessages);
3803
4091
  }
4092
+ const p = parsed;
4093
+ const statusAssessment = p.statusAssessment ?? null;
4094
+ const sessionTypeRaw = p.sessionType;
3804
4095
  return {
3805
- boundaries: (parsed.boundaries ?? []).filter((b) => typeof b?.afterIndex === "number").map((b) => ({
3806
- afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
3807
- topic: String(b.topic ?? "").slice(0, 100),
3808
- priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
3809
- confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
3810
- })),
3811
- mainGoal: parsed.mainGoal ?? "",
3812
- sessionType: ["implementation", "review", "debugging", "discussion"].includes(parsed.sessionType) ? parsed.sessionType : "implementation",
3813
- enrichedConstraints: Array.isArray(parsed.enrichedConstraints) ? parsed.enrichedConstraints.map(String) : [],
3814
- crossReferences: Array.isArray(parsed.crossReferences) ? parsed.crossReferences.map(String) : [],
4096
+ boundaries: normalizeBoundaries(p.boundaries, llmMessages.length),
4097
+ mainGoal: typeof p.mainGoal === "string" ? p.mainGoal : "",
4098
+ sessionType: typeof sessionTypeRaw === "string" && SESSION_TYPES.includes(sessionTypeRaw) ? sessionTypeRaw : "implementation",
4099
+ enrichedConstraints: stringArray(p.enrichedConstraints),
4100
+ crossReferences: stringArray(p.crossReferences),
3815
4101
  statusAssessment: {
3816
- done: Array.isArray(parsed.statusAssessment?.done) ? parsed.statusAssessment.done.map(String) : [],
3817
- inProgress: Array.isArray(parsed.statusAssessment?.inProgress) ? parsed.statusAssessment.inProgress.map(String) : [],
3818
- blocked: Array.isArray(parsed.statusAssessment?.blocked) ? parsed.statusAssessment.blocked.map(String) : []
4102
+ done: stringArray(statusAssessment?.done),
4103
+ inProgress: stringArray(statusAssessment?.inProgress),
4104
+ blocked: stringArray(statusAssessment?.blocked)
3819
4105
  },
3820
- criticalContext: Array.isArray(parsed.criticalContext) ? parsed.criticalContext.map(String) : [],
3821
- keyDecisions: Array.isArray(parsed.keyDecisions) ? parsed.keyDecisions.map(String) : []
4106
+ criticalContext: stringArray(p.criticalContext),
4107
+ keyDecisions: stringArray(p.keyDecisions)
3822
4108
  };
3823
4109
  }
3824
4110
  function fallbackExplorationReport(llmMessages) {
@@ -3841,12 +4127,12 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
3841
4127
  "Main goal: " + (extraction.mainGoal ?? "unknown"),
3842
4128
  "Files modified (" + extraction.modifiedFiles.length + "): " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none"),
3843
4129
  "Files read (" + extraction.readFiles.length + "): " + (extraction.readFiles.join(", ") || "none"),
3844
- "Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " (resolved)" : e.retryAttempted ? " (retry attempted)" : "")).join("; ") || "none"),
3845
- "Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 80)).join("; ") || "none"),
3846
- "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"),
3847
4133
  "Heuristic topics (" + extraction.topics.length + "): " + (extraction.topics.map((t) => "[" + t.startIndex + "-" + t.endIndex + "] " + t.type).join("; ") || "none"),
3848
- extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0, 100)).join(" | ") : "",
3849
- 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(" | ") : ""
3850
4136
  ].filter(Boolean).join(`
3851
4137
  `);
3852
4138
  const userContent = `Explore this conversation and produce the structured report.
@@ -3886,7 +4172,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
3886
4172
  toolSupport.set(cacheKey, true, svc.clock.now());
3887
4173
  const messages = [
3888
4174
  { role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() },
3889
- { role: "assistant", content: probeResp.content, timestamp: Date.now() }
4175
+ probeResp
3890
4176
  ];
3891
4177
  for (const tc of toolCalls) {
3892
4178
  const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
@@ -3920,7 +4206,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
3920
4206
  }
3921
4207
  return { report: report2, rounds, toolSupported: true };
3922
4208
  }
3923
- messages.push({ role: "assistant", content: response.content, timestamp: Date.now() });
4209
+ messages.push(response);
3924
4210
  for (const tc of nextToolCalls) {
3925
4211
  const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
3926
4212
  messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
@@ -3961,7 +4247,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
3961
4247
  return { report, rounds: 1, toolSupported: supportsTools };
3962
4248
  }
3963
4249
  async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal, services) {
3964
- 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(`
3965
4251
  `);
3966
4252
  const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
3967
4253
 
@@ -3987,10 +4273,10 @@ User steering: ` + userNote : "");
3987
4273
  }
3988
4274
  }
3989
4275
  async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal, services) {
3990
- 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(`
3991
4277
  ---
3992
4278
  `);
3993
- 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(`
3994
4280
  `);
3995
4281
  const prompt = `Analyze this conversation and produce a JSON report.
3996
4282
 
@@ -3999,9 +4285,9 @@ First user messages:
3999
4285
 
4000
4286
  Deterministic data:
4001
4287
  ` + "- Files modified: " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none") + `
4002
- - Errors: ` + (extraction.errors.map((e) => e.message.slice(0, 80)).join("; ") || "none") + `
4003
- - Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0, 80)).join("; ") || "none") + `
4004
- - 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") + `
4005
4291
 
4006
4292
  Last 30 messages:
4007
4293
  ` + last30 + (prevSummary ? `
@@ -4114,7 +4400,7 @@ Session-specific instructions:
4114
4400
  async function summarizeBatch(batch, extraction, model, auth, signal, services) {
4115
4401
  const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
4116
4402
  const extractionCtx = buildExtractionContext(extraction, range);
4117
- 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) : ""));
4118
4404
  const decisionCtx = activeDecisions.length ? `
4119
4405
  ## Active Decisions from previous segments (honour these):
4120
4406
  ` + activeDecisions.join(`
@@ -4124,8 +4410,8 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
4124
4410
  return "--- CHUNK " + id + ": " + ch.topic + " (" + ch.priority + `) ---
4125
4411
  ` + ch.messages.map((m) => {
4126
4412
  const role = m?.role ?? "unknown";
4127
- const content = extractText(m?.content).slice(0, 500);
4128
- 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("; ");
4129
4415
  const toolSuffix = toolCalls ? `
4130
4416
  [tool_calls] ` + toolCalls : "";
4131
4417
  return "[" + role + "] " + content + toolSuffix;
@@ -4167,9 +4453,9 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
4167
4453
  const prio = f("Priority").toLowerCase();
4168
4454
  const sectionFallback = sec.split(`
4169
4455
  `).slice(1).join(`
4170
- `).trim().slice(0, 500);
4171
- const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0, 180)).join(`
4172
- `).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);
4173
4459
  return {
4174
4460
  topic: ch.topic,
4175
4461
  startIndex: ch.startIndex,
@@ -4206,18 +4492,18 @@ function assembleFallback(summaries, extraction) {
4206
4492
  extraction.mainGoal ?? "See topics below.",
4207
4493
  "",
4208
4494
  "## Constraints & Preferences",
4209
- ...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)),
4210
4496
  "",
4211
4497
  "## Progress",
4212
4498
  "### Done",
4213
4499
  "- See topics below",
4214
4500
  "### In Progress",
4215
- ...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)),
4216
4502
  "### Blocked",
4217
4503
  "- None",
4218
4504
  "",
4219
4505
  "## Key Decisions",
4220
- ...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 : "")),
4221
4507
  "",
4222
4508
  "## Files Modified",
4223
4509
  ...detModified.map((f) => "- " + f),
@@ -4229,13 +4515,26 @@ function assembleFallback(summaries, extraction) {
4229
4515
  "1. See topics below",
4230
4516
  "",
4231
4517
  "## Critical Context",
4232
- ...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)),
4233
4519
  "",
4234
4520
  "## Topics Covered",
4235
- ...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))
4236
4522
  ].join(`
4237
4523
  `);
4238
4524
  }
4525
+ function failedChunkSummary(ch) {
4526
+ return {
4527
+ topic: ch.topic,
4528
+ startIndex: ch.startIndex,
4529
+ endIndex: ch.endIndex,
4530
+ summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
4531
+ `).slice(0, TRUNC.DETAIL),
4532
+ keyDecisions: [],
4533
+ filesModified: [],
4534
+ filesRead: [],
4535
+ priority: ch.priority
4536
+ };
4537
+ }
4239
4538
 
4240
4539
  // src/app/steps/synthesize.ts
4241
4540
  init_constants();
@@ -4354,10 +4653,15 @@ async function summarizeConversation(rc) {
4354
4653
  }
4355
4654
  const concurrency = rc.providerCaps.concurrencyLimit;
4356
4655
  if (totalBatches <= 1) {
4357
- try {
4358
- summaries.push(...await summarizeBatch(batches[0], extraction, rc.summaryModel, rc.summaryAuth, rc.cancellation.signal, rc.services));
4359
- } catch (err) {
4360
- summaries.push(...batches[0].map((ch) => failedChunkSummary(ch)));
4656
+ const single = batches[0];
4657
+ if (single) {
4658
+ try {
4659
+ summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, rc.summaryAuth, rc.cancellation.signal, rc.services));
4660
+ } catch (err) {
4661
+ summaries.push(...single.map((ch) => failedChunkSummary(ch)));
4662
+ }
4663
+ } else {
4664
+ rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
4361
4665
  }
4362
4666
  } else {
4363
4667
  const results = new Array(totalBatches);
@@ -4434,19 +4738,6 @@ async function summarizeConversation(rc) {
4434
4738
  markMeasuredPhase(out, "synthesize", synthPhaseStart);
4435
4739
  return advance(out, "_synthesized");
4436
4740
  }
4437
- function failedChunkSummary(ch) {
4438
- return {
4439
- topic: ch.topic,
4440
- startIndex: ch.startIndex,
4441
- endIndex: ch.endIndex,
4442
- summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
4443
- `).slice(0, 300),
4444
- keyDecisions: [],
4445
- filesModified: [],
4446
- filesRead: [],
4447
- priority: ch.priority
4448
- };
4449
- }
4450
4741
 
4451
4742
  // src/phases/verify.ts
4452
4743
  init_constants();
@@ -4639,6 +4930,16 @@ function appendToSection(summary, kind, text, fallbackBody = "") {
4639
4930
  ` : "") + text.trim());
4640
4931
  }
4641
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
+
4642
4943
  // src/phases/verify.ts
4643
4944
  function verifySummary(summary, extraction) {
4644
4945
  const parsed = parseSummary(summary);
@@ -4670,22 +4971,22 @@ function verifySummary(summary, extraction) {
4670
4971
  }
4671
4972
  }
4672
4973
  for (const e of extraction.errors.filter((e2) => !e2.resolved)) {
4673
- const snippet = e.message.slice(0, 30).toLowerCase();
4974
+ const snippet = e.message.trim().replace(/\s+/g, " ").slice(0, TRUNC.ERROR_SNIPPET).toLowerCase();
4674
4975
  if (snippet.length > 5 && !lower.includes(snippet)) {
4675
- gaps.push("Missing error: " + e.message.slice(0, 80));
4976
+ gaps.push("Missing error: " + e.message.slice(0, TRUNC.SNIPPET));
4676
4977
  score -= 5;
4677
4978
  }
4678
4979
  }
4679
4980
  for (const c of extraction.constraints.filter((c2) => c2.confidence >= 0.8)) {
4680
- const keywords = c.text.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
4981
+ const keywords = extractCheckKeywords(c.text, 3);
4681
4982
  const found = keywords.some((k) => lower.includes(k.toLowerCase()));
4682
4983
  if (!found && keywords.length > 0) {
4683
- gaps.push("Missing constraint: " + c.text.slice(0, 100));
4984
+ gaps.push("Missing constraint: " + c.text.slice(0, TRUNC.TOPIC_LABEL));
4684
4985
  score -= 3;
4685
4986
  }
4686
4987
  }
4687
4988
  if (extraction.mainGoal) {
4688
- const goalWords = extraction.mainGoal.split(/\s+/).filter((w) => w.length > 3).slice(0, 4);
4989
+ const goalWords = extractCheckKeywords(extraction.mainGoal, 4);
4689
4990
  const goalFound = goalWords.some((w) => lower.includes(w.toLowerCase()));
4690
4991
  if (!goalFound) {
4691
4992
  gaps.push("Main goal may be missing from summary");
@@ -4728,9 +5029,9 @@ function verifySummary(summary, extraction) {
4728
5029
  const decisionSection = findSection(parsed, "decisions");
4729
5030
  const decisionBody = decisionSection?.body.toLowerCase() ?? "";
4730
5031
  for (const d of highConfDecisions) {
4731
- const keywords = d.summary.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
5032
+ const keywords = extractCheckKeywords(d.summary, 3);
4732
5033
  if (keywords.length > 0 && !keywords.some((k) => decisionBody.includes(k.toLowerCase()))) {
4733
- gaps.push("Missing decision: " + d.summary.slice(0, 100));
5034
+ gaps.push("Missing decision: " + d.summary.slice(0, TRUNC.TOPIC_LABEL));
4734
5035
  score -= 3;
4735
5036
  }
4736
5037
  }
@@ -4909,7 +5210,7 @@ function loadCompactionState(projectId) {
4909
5210
  updatedAt = 0;
4910
5211
  }
4911
5212
  }
4912
- if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
5213
+ if (Date.now() - updatedAt > SEVEN_DAYS_MS)
4913
5214
  return null;
4914
5215
  }
4915
5216
  return data;
@@ -4918,17 +5219,18 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
4918
5219
  let decisionId = 0;
4919
5220
  let constraintId = 0;
4920
5221
  let errorId = 0;
5222
+ const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
4921
5223
  return {
4922
5224
  goal: extraction.mainGoal,
4923
5225
  decisions: extraction.decisions.map((d) => ({
4924
- id: "decision-" + ++decisionId,
4925
- summary: d.summary.slice(0, 200),
4926
- ...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) } : {},
4927
5229
  type: d.type
4928
5230
  })),
4929
5231
  constraints: extraction.constraints.map((c) => ({
4930
5232
  id: "constraint-" + ++constraintId,
4931
- text: c.text.slice(0, 300),
5233
+ text: c.text.slice(0, TRUNC.CONSTRAINT_TEXT),
4932
5234
  category: c.category,
4933
5235
  confidence: c.confidence
4934
5236
  })),
@@ -4936,17 +5238,18 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
4936
5238
  readFiles: extraction.readFiles,
4937
5239
  deletedFiles: extraction.deletedFiles,
4938
5240
  unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
4939
- const bn = extraction.modifiedFiles.find((f) => e.message.toLowerCase().includes(f.path.split("/").pop()?.toLowerCase() ?? "__none__"));
5241
+ const msgLower = e.message.toLowerCase();
5242
+ const match = fileNeedles.find(({ needles }) => needles.some((n) => msgLower.includes(n)));
4940
5243
  return {
4941
- id: "error-" + ++errorId,
4942
- message: e.message.slice(0, 300),
5244
+ id: ID_PREFIX.ERROR + ++errorId,
5245
+ message: e.message.slice(0, TRUNC.MESSAGE),
4943
5246
  tool: e.tool,
4944
- files: bn ? [bn.path] : []
5247
+ files: match ? [match.path] : []
4945
5248
  };
4946
5249
  }),
4947
5250
  resolvedErrors: extraction.errors.filter((e) => e.resolved).map((e) => ({
4948
- id: "error-" + ++errorId,
4949
- message: e.message.slice(0, 300),
5251
+ id: ID_PREFIX.ERROR + ++errorId,
5252
+ message: e.message.slice(0, TRUNC.MESSAGE),
4950
5253
  tool: e.tool
4951
5254
  })),
4952
5255
  openLoops,
@@ -4976,28 +5279,28 @@ function injectOpenLoopsSection(summary, openLoops) {
4976
5279
  return renderSummary(updated);
4977
5280
  }
4978
5281
  function computeDelta(prev, current) {
4979
- const prevDecisionTexts = new Set(prev.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
4980
- const currDecisionTexts = new Set(current.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
4981
- const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
4982
- const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
4983
- const prevLoopSummaries = new Map(prev.openLoops.map((l) => [l.summary.toLowerCase().slice(0, 50), l]));
4984
- 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)));
4985
5289
  const resolvedLoops = [];
4986
5290
  const persistentLoops = [];
4987
- for (const [key, loop] of prevLoopSummaries) {
4988
- if (currLoopSummaries.has(key)) {
5291
+ for (const [k, loop] of prevLoopSummaries) {
5292
+ if (currLoopKeys.has(k))
4989
5293
  persistentLoops.push(loop.summary);
4990
- } else {
5294
+ else
4991
5295
  resolvedLoops.push(loop.summary);
4992
- }
4993
5296
  }
4994
- 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);
4995
5298
  const prevFiles = new Set(prev.modifiedFiles);
4996
5299
  const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
4997
- const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
4998
- const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
4999
- const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(e.message.toLowerCase().slice(0, 40))).map((e) => e.message);
5000
- 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);
5001
5304
  const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
5002
5305
  return {
5003
5306
  newDecisions,
@@ -5018,22 +5321,22 @@ function formatDeltaSection(delta) {
5018
5321
  lines.push("- **Goal shifted**: " + (delta.previousGoal ?? "?") + " \u2192 see current goal above");
5019
5322
  }
5020
5323
  if (delta.resolvedLoops.length) {
5021
- 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(", "));
5022
5325
  }
5023
5326
  if (delta.persistentLoops.length) {
5024
- 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("; "));
5025
5328
  }
5026
5329
  if (delta.newLoops.length) {
5027
- 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("; "));
5028
5331
  }
5029
5332
  if (delta.newDecisions.length) {
5030
- 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("; "));
5031
5334
  }
5032
5335
  if (delta.resolvedErrors.length) {
5033
- 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("; "));
5034
5337
  }
5035
5338
  if (delta.newErrors.length) {
5036
- 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("; "));
5037
5340
  }
5038
5341
  if (delta.newModifiedFiles.length) {
5039
5342
  lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
@@ -5055,81 +5358,38 @@ function injectDeltaSection(summary, delta) {
5055
5358
  const updated = upsertSection(parsed, "changes", body, placement);
5056
5359
  return renderSummary(updated);
5057
5360
  }
5361
+ function ensurePinnedPaths(summary, pinned) {
5362
+ if (!pinned.length)
5363
+ return summary;
5364
+ const lower = summary.toLowerCase();
5365
+ const missing = pinned.filter((p) => p && p.trim().length > 0 && !lower.includes(p.toLowerCase()));
5366
+ if (!missing.length)
5367
+ return summary;
5368
+ const parsed = parseSummary(summary);
5369
+ const updated = appendToSection(parsed, "files-read", missing.map((p) => "- " + p).join(`
5370
+ `), "- Pinned by config (always preserved):");
5371
+ return renderSummary(updated);
5372
+ }
5058
5373
  function extractNextActions(summary) {
5059
- const match = summary.match(/## Next Steps\s*\n([\s\S]*?)(?=##|$)/);
5060
- if (!match)
5374
+ const section = findSection(summary, "next-steps");
5375
+ if (!section)
5061
5376
  return [];
5062
- return match[1].split(`
5377
+ return section.body.split(`
5063
5378
  `).map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 0);
5064
5379
  }
5065
5380
  function extractCriticalContext(summary) {
5066
- const match = summary.match(/## Critical Context\s*\n([\s\S]*?)(?=##|$)/);
5067
- if (!match)
5381
+ const section = findSection(summary, "critical-context");
5382
+ if (!section)
5068
5383
  return [];
5069
- return match[1].split(`
5384
+ return section.body.split(`
5070
5385
  `).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
5071
5386
  }
5072
5387
 
5073
- // src/app/steps/state.ts
5074
- init_tokens();
5075
- function buildState(rc) {
5076
- const extraction = rc.extraction;
5077
- let summary = rc.finalSummary;
5078
- const openLoops = extractOpenLoops(rc.llmMessages, extraction);
5079
- if (openLoops.length > 0) {
5080
- rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
5081
- summary = injectOpenLoopsSection(summary, openLoops);
5082
- }
5083
- const nextActions = extractNextActions(summary);
5084
- const criticalContextItems = extractCriticalContext(summary);
5085
- const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
5086
- const prevState = loadCompactionState(rc.projectId);
5087
- if (prevState) {
5088
- const delta = computeDelta(prevState, compactionState);
5089
- if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
5090
- summary = injectDeltaSection(summary, delta);
5091
- rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
5092
- }
5093
- }
5094
- const detModified = extraction.modifiedFiles.map((f) => f.path);
5095
- const detRead = extraction.readFiles;
5096
- const estimatedAfter = estimateTokens(summary) + rc.accTokens;
5097
- const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
5098
- const details = {
5099
- method: rc.method,
5100
- chunkCount: rc.chunkCount || 1,
5101
- topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
5102
- readFiles: detRead,
5103
- modifiedFiles: detModified,
5104
- totalMessages: rc.toCompact.length,
5105
- totalTokensSummarized: rc.convTokens,
5106
- llmCalls: rc.llmCalls,
5107
- profile: rc.profile,
5108
- backupPath: rc.backupPath,
5109
- tokensSaved,
5110
- verified: rc.verified,
5111
- gaps: rc.verificationGaps,
5112
- explorationRounds: rc.explorationRounds,
5113
- explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
5114
- model: rc.modelLabel,
5115
- qualityScore: rc.verificationScore,
5116
- tokensBefore: rc.totalTokens,
5117
- compactionState,
5118
- openLoops
5119
- };
5120
- const out = rc;
5121
- out.finalSummary = summary;
5122
- out.openLoops = openLoops;
5123
- out.compactionState = compactionState;
5124
- out.details = details;
5125
- out.tokensSaved = tokensSaved;
5126
- return advance(out, "_stated");
5127
- }
5128
-
5129
5388
  // src/utils/damage.ts
5130
5389
  init_logger();
5131
5390
  init_paths();
5132
5391
  init_fs();
5392
+ init_constants();
5133
5393
  var COMPLAINT_PATTERNS = [
5134
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,
5135
5395
  /(?:that'?s? not (?:what I|right)|that'?s? wrong|yanl\u0131\u015F|hay\u0131r de\u011Fil|no that'|that doesn'?t match)/i,
@@ -5137,6 +5397,7 @@ var COMPLAINT_PATTERNS = [
5137
5397
  ];
5138
5398
  function detectDamage(postMessages, details) {
5139
5399
  const signals = [];
5400
+ const reReadFiles = [];
5140
5401
  const compactedFiles = new Set(details.modifiedFiles.map((f) => f.toLowerCase()));
5141
5402
  const compactedReadFiles = new Set(details.readFiles.map((f) => f.toLowerCase()));
5142
5403
  for (let i = 0;i < postMessages.length; i++) {
@@ -5145,8 +5406,8 @@ function detectDamage(postMessages, details) {
5145
5406
  if (msg.role === "assistant") {
5146
5407
  const blocks = Array.isArray(msg.content) ? msg.content : [];
5147
5408
  for (const b of blocks) {
5148
- if (isToolCallBlock(b) && (b.name === "read" || b.name === "bash")) {
5149
- const fp = b.arguments?.path ?? b.arguments?.file_path;
5409
+ if (isToolCallBlock(b) && classifyTool(b.arguments) === "accesses") {
5410
+ const fp = extractToolPath(b.arguments);
5150
5411
  if (fp) {
5151
5412
  const fpLower = fp.toLowerCase();
5152
5413
  if (compactedFiles.has(fpLower) || compactedReadFiles.has(fpLower)) {
@@ -5155,6 +5416,8 @@ function detectDamage(postMessages, details) {
5155
5416
  severity: "medium",
5156
5417
  detail: "Agent re-read compacted file: " + fp
5157
5418
  });
5419
+ if (!reReadFiles.includes(fp))
5420
+ reReadFiles.push(fp);
5158
5421
  }
5159
5422
  }
5160
5423
  }
@@ -5166,18 +5429,18 @@ function detectDamage(postMessages, details) {
5166
5429
  signals.push({
5167
5430
  type: "user-complaint",
5168
5431
  severity: "high",
5169
- detail: 'User complaint after compaction: "' + text.slice(0, 100) + '"'
5432
+ detail: 'User complaint after compaction: "' + text.slice(0, TRUNC.TOPIC_LABEL) + '"'
5170
5433
  });
5171
5434
  break;
5172
5435
  }
5173
5436
  }
5174
5437
  for (const t of details.topics) {
5175
- const topicWords = t.toLowerCase().split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
5176
- 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()))) {
5177
5440
  signals.push({
5178
5441
  type: "re-question",
5179
5442
  severity: "low",
5180
- detail: "User mentions compacted topic: " + t.slice(0, 80)
5443
+ detail: "User mentions compacted topic: " + t.slice(0, TRUNC.SNIPPET)
5181
5444
  });
5182
5445
  }
5183
5446
  }
@@ -5206,7 +5469,8 @@ function detectDamage(postMessages, details) {
5206
5469
  return {
5207
5470
  signals,
5208
5471
  damageScore,
5209
- summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)"
5472
+ summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)",
5473
+ reReadFiles
5210
5474
  };
5211
5475
  }
5212
5476
  function logDamageReport(sessionId, report, details) {
@@ -5226,6 +5490,92 @@ function logDamageReport(sessionId, report, details) {
5226
5490
  warn("logDamageReport failed", e);
5227
5491
  }
5228
5492
  }
5493
+ var REMEDIATION_TTL_MS = SEVEN_DAYS_MS;
5494
+ function writeRemediationHints(projectId, files) {
5495
+ if (!files.length)
5496
+ return;
5497
+ const cleaned = [...new Set(files.map((f) => (f ?? "").trim()).filter((f) => f.length > 0))];
5498
+ if (!cleaned.length)
5499
+ return;
5500
+ try {
5501
+ writeJsonSync(remediationHintsFile(projectId), { files: cleaned, updatedAt: Date.now() });
5502
+ } catch (e) {
5503
+ warn("writeRemediationHints failed", e);
5504
+ }
5505
+ }
5506
+ function readRemediationHints(projectId) {
5507
+ const data = readJsonSync(remediationHintsFile(projectId));
5508
+ if (!data || !Array.isArray(data.files))
5509
+ return [];
5510
+ if (typeof data.updatedAt === "number" && Date.now() - data.updatedAt > REMEDIATION_TTL_MS)
5511
+ return [];
5512
+ return data.files.filter((f) => typeof f === "string");
5513
+ }
5514
+
5515
+ // src/app/steps/state.ts
5516
+ init_tokens();
5517
+ function buildState(rc) {
5518
+ const extraction = rc.extraction;
5519
+ let summary = rc.finalSummary;
5520
+ const openLoops = extractOpenLoops(rc.llmMessages, extraction);
5521
+ if (openLoops.length > 0) {
5522
+ rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
5523
+ summary = injectOpenLoopsSection(summary, openLoops);
5524
+ }
5525
+ const pinPaths = rc.config.pinPaths ?? [];
5526
+ const remediated = readRemediationHints(rc.projectId);
5527
+ const preserve = remediated.length ? Array.from(new Set([...pinPaths, ...remediated])) : pinPaths;
5528
+ if (remediated.length) {
5529
+ rc.notify("Remediation: re-preserving " + remediated.length + " file(s) lost in a prior compaction", "info");
5530
+ }
5531
+ if (preserve.length > 0) {
5532
+ summary = ensurePinnedPaths(summary, preserve);
5533
+ }
5534
+ const nextActions = extractNextActions(summary);
5535
+ const criticalContextItems = extractCriticalContext(summary);
5536
+ const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
5537
+ const prevState = loadCompactionState(rc.projectId);
5538
+ if (prevState) {
5539
+ const delta = computeDelta(prevState, compactionState);
5540
+ if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
5541
+ summary = injectDeltaSection(summary, delta);
5542
+ rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
5543
+ }
5544
+ }
5545
+ const detModified = extraction.modifiedFiles.map((f) => f.path);
5546
+ const detRead = extraction.readFiles;
5547
+ const estimatedAfter = estimateTokens(summary) + rc.accTokens;
5548
+ const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
5549
+ const details = {
5550
+ method: rc.method,
5551
+ chunkCount: rc.chunkCount || 1,
5552
+ topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
5553
+ readFiles: detRead,
5554
+ modifiedFiles: detModified,
5555
+ totalMessages: rc.toCompact.length,
5556
+ totalTokensSummarized: rc.convTokens,
5557
+ llmCalls: rc.llmCalls,
5558
+ profile: rc.profile,
5559
+ backupPath: rc.backupPath,
5560
+ tokensSaved,
5561
+ verified: rc.verified,
5562
+ gaps: rc.verificationGaps,
5563
+ explorationRounds: rc.explorationRounds,
5564
+ explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
5565
+ model: rc.modelLabel,
5566
+ qualityScore: rc.verificationScore,
5567
+ tokensBefore: rc.totalTokens,
5568
+ compactionState,
5569
+ openLoops
5570
+ };
5571
+ const out = rc;
5572
+ out.finalSummary = summary;
5573
+ out.openLoops = openLoops;
5574
+ out.compactionState = compactionState;
5575
+ out.details = details;
5576
+ out.tokensSaved = tokensSaved;
5577
+ return advance(out, "_stated");
5578
+ }
5229
5579
 
5230
5580
  // src/app/steps/persist.ts
5231
5581
  import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
@@ -5236,7 +5586,7 @@ function persistDurableState(rc) {
5236
5586
  }
5237
5587
  function runDamageDetection(rc) {
5238
5588
  try {
5239
- const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([e.message])).flat();
5589
+ const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([asBranchMessage(e.message)])).flat();
5240
5590
  if (postCompactMsgs.length <= 2)
5241
5591
  return;
5242
5592
  const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
@@ -5252,6 +5602,9 @@ function runDamageDetection(rc) {
5252
5602
  rc.notify("Previous compaction damage: " + damage.summary, "warning");
5253
5603
  }
5254
5604
  logDamageReport(rc.sessionId, damage, safeDetails);
5605
+ if (damage.reReadFiles.length > 0) {
5606
+ writeRemediationHints(rc.projectId, damage.reReadFiles);
5607
+ }
5255
5608
  } catch (err) {
5256
5609
  warn("Damage detection error", err);
5257
5610
  }
@@ -5461,8 +5814,16 @@ async function runSmartCompact(opts) {
5461
5814
  recordSuccessMetrics(stated, "success");
5462
5815
  if (!stated.flags.autoTriggered) {
5463
5816
  try {
5464
- const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 5000));
5465
- await Promise.race([showResultScreen(stated.ctx, stated.details, stated.extraction, stated.services), timeoutPromise]);
5817
+ let resultTimer;
5818
+ const timeoutPromise = new Promise((resolve) => {
5819
+ resultTimer = setTimeout(resolve, 5000);
5820
+ });
5821
+ try {
5822
+ await Promise.race([showResultScreen(stated.ctx, stated.details, stated.extraction, stated.services), timeoutPromise]);
5823
+ } finally {
5824
+ if (resultTimer)
5825
+ clearTimeout(resultTimer);
5826
+ }
5466
5827
  } catch (err) {
5467
5828
  warn("Result screen error", err);
5468
5829
  stated.notify("Result screen skipped", "info");
@@ -5571,40 +5932,35 @@ function unwrapConsumed(result, ctx) {
5571
5932
  return null;
5572
5933
  }
5573
5934
  }
5574
- function resolveModelArg(ctx, modelArg) {
5575
- const [p, ...r] = modelArg.split("/");
5935
+ function findModelById(ctx, modelId) {
5936
+ const [p, ...r] = modelId.split("/");
5576
5937
  return ctx.modelRegistry.find(p, r.join("/"));
5577
5938
  }
5578
5939
  function resolveModels(ctx, primary, config) {
5579
5940
  const fallback = primary ?? ctx.model;
5580
5941
  const available = ctx.modelRegistry.getAvailable();
5581
5942
  let sumModel = fallback;
5582
- const configuredSumModels = [config.summaryModel].filter(Boolean);
5583
- for (const modelId of configuredSumModels) {
5584
- const [p, ...r] = modelId.split("/");
5585
- const found = ctx.modelRegistry.find(p, r.join("/"));
5586
- if (found) {
5943
+ if (config.summaryModel) {
5944
+ const found = findModelById(ctx, config.summaryModel);
5945
+ if (found)
5587
5946
  sumModel = found;
5588
- break;
5589
- }
5590
5947
  }
5591
5948
  if (sumModel === fallback && !fallback)
5592
5949
  sumModel = available[0];
5593
5950
  let segModel = sumModel;
5594
5951
  if (config.segmentationModel) {
5595
- const [p, ...r] = config.segmentationModel.split("/");
5596
- segModel = ctx.modelRegistry.find(p, r.join("/")) ?? sumModel;
5952
+ segModel = findModelById(ctx, config.segmentationModel) ?? sumModel;
5597
5953
  }
5598
5954
  return { segModel, sumModel };
5599
5955
  }
5600
5956
  function smartCompactExtension(pi) {
5601
- const PENDING_TTL_MS = 5 * 60 * 1000;
5957
+ const PENDING_TTL_MS = FIVE_MINUTES_MS;
5602
5958
  const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
5603
5959
  const isRunning = { value: false };
5604
5960
  pi.registerCommand("smart-compact", {
5605
- description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
5961
+ description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run|restore|metrics|dashboard] [note]",
5606
5962
  getArgumentCompletions: (prefix) => {
5607
- const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
5963
+ const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "restore", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
5608
5964
  return m.length ? m : null;
5609
5965
  },
5610
5966
  handler: async (args, ctx) => {
@@ -5633,6 +5989,48 @@ function smartCompactExtension(pi) {
5633
5989
  }
5634
5990
  return;
5635
5991
  }
5992
+ if (flags.includes("restore")) {
5993
+ const backups = listBackups();
5994
+ if (!backups.length) {
5995
+ ctx.ui.notify("No smart-compact backups found", "info");
5996
+ return;
5997
+ }
5998
+ const selected = await showRestorePicker(ctx, backups);
5999
+ if (!selected) {
6000
+ ctx.ui.notify("Cancelled", "info");
6001
+ return;
6002
+ }
6003
+ const content = readBackupContent(selected);
6004
+ if (!content) {
6005
+ ctx.ui.notify("Could not read backup: " + selected, "error");
6006
+ return;
6007
+ }
6008
+ const action = await showRestoreAction(ctx, selected);
6009
+ if (action === "restore") {
6010
+ const branch = ctx.sessionManager.getBranch();
6011
+ const leafId = branch.length ? branch[branch.length - 1].id : undefined;
6012
+ if (!leafId) {
6013
+ ctx.ui.notify("Cannot restore: no session leaf to fork from \u2014 showing content instead", "warning");
6014
+ await showBackupViewer(ctx, content, selected);
6015
+ return;
6016
+ }
6017
+ try {
6018
+ const result = await ctx.fork(leafId, {
6019
+ withSession: async (rctx) => {
6020
+ await rctx.sendMessage(buildRestoreMessage(content, selected), { deliverAs: "nextTurn" });
6021
+ }
6022
+ });
6023
+ ctx.ui.notify(result.cancelled ? "Restore cancelled" : "Restored backup into a new session", "info");
6024
+ } catch (e) {
6025
+ warn("Restore fork failed", e);
6026
+ ctx.ui.notify("Restore failed (" + (e instanceof Error ? e.message : String(e)) + ") \u2014 showing content instead", "warning");
6027
+ await showBackupViewer(ctx, content, selected);
6028
+ }
6029
+ } else if (action === "view") {
6030
+ await showBackupViewer(ctx, content, selected);
6031
+ }
6032
+ return;
6033
+ }
5636
6034
  const modelArg = tokens.find((t) => t.includes("/"));
5637
6035
  const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
5638
6036
  const profile = profileArg ?? loadConfig().profile;
@@ -5661,7 +6059,7 @@ function smartCompactExtension(pi) {
5661
6059
  await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning, force: true });
5662
6060
  return;
5663
6061
  }
5664
- 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());
5665
6063
  if (!sumModel) {
5666
6064
  ctx.ui.notify("Could not resolve model", "error");
5667
6065
  return;