pi-smart-compact 7.13.2 → 7.15.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 (36) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/app/pending-slot.d.ts +88 -0
  3. package/dist/app/pending-slot.d.ts.map +1 -0
  4. package/dist/app/run-context.d.ts +12 -10
  5. package/dist/app/run-context.d.ts.map +1 -1
  6. package/dist/app/run-smart-compact.d.ts +13 -8
  7. package/dist/app/run-smart-compact.d.ts.map +1 -1
  8. package/dist/app/steps/persist.d.ts.map +1 -1
  9. package/dist/app/steps/window.d.ts.map +1 -1
  10. package/dist/constants.d.ts +7 -1
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +291 -150
  14. package/dist/infra/session-identity.d.ts +54 -0
  15. package/dist/infra/session-identity.d.ts.map +1 -0
  16. package/dist/phases/verify.d.ts.map +1 -1
  17. package/dist/types.d.ts +22 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/ui/overlays.d.ts +3 -3
  20. package/dist/ui/overlays.d.ts.map +1 -1
  21. package/dist/utils/cache.d.ts +0 -2
  22. package/dist/utils/cache.d.ts.map +1 -1
  23. package/dist/utils/extraction.d.ts +22 -0
  24. package/dist/utils/extraction.d.ts.map +1 -1
  25. package/dist/utils/file-needles.d.ts +44 -0
  26. package/dist/utils/file-needles.d.ts.map +1 -0
  27. package/dist/utils/file-ref-detect.d.ts +50 -0
  28. package/dist/utils/file-ref-detect.d.ts.map +1 -0
  29. package/dist/utils/helpers.d.ts +1 -15
  30. package/dist/utils/helpers.d.ts.map +1 -1
  31. package/dist/utils/lru.d.ts +31 -0
  32. package/dist/utils/lru.d.ts.map +1 -0
  33. package/dist/utils/pruning.d.ts.map +1 -1
  34. package/dist/utils/session-log.d.ts +31 -0
  35. package/dist/utils/session-log.d.ts.map +1 -1
  36. package/package.json +10 -8
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.13.2", 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.15.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}
@@ -848,11 +848,8 @@ var init_services = __esm(() => {
848
848
  import fs3 from "fs";
849
849
  import path4 from "path";
850
850
  import crypto5 from "crypto";
851
- function getCompactSessionId() {
852
- if (!_compactSessionId) {
853
- _compactSessionId = "sc-" + Date.now().toString(36) + "-" + crypto5.randomBytes(4).toString("hex");
854
- }
855
- return _compactSessionId;
851
+ function fallbackSessionId() {
852
+ return "sc-" + Date.now().toString(36) + "-" + crypto5.randomBytes(4).toString("hex");
856
853
  }
857
854
  function cacheOpts(opts, provider, phase, services) {
858
855
  if (phase && INTERNAL_PHASES.has(phase)) {
@@ -863,7 +860,7 @@ function cacheOpts(opts, provider, phase, services) {
863
860
  if (retention === "none") {
864
861
  return { ...opts, cacheRetention: "none" };
865
862
  }
866
- return { ...opts, sessionId: services?.compactSessionId ?? getCompactSessionId(), cacheRetention: retention };
863
+ return { ...opts, sessionId: services?.compactSessionId ?? fallbackSessionId(), cacheRetention: retention };
867
864
  }
868
865
  function recordMetric(m, services = getDefaultServices()) {
869
866
  services.metrics.record(m);
@@ -1313,7 +1310,7 @@ function writeMetricsDashboard(entries = readMetricsLog(200)) {
1313
1310
  return null;
1314
1311
  }
1315
1312
  }
1316
- var _compactSessionId = null, INTERNAL_PHASES, EXTRACTION_CACHE_TTL_MS, EXTRACTION_CACHE_PRUNE_MAX_AGE_MS, _extractionPruneInFlight = false;
1313
+ var INTERNAL_PHASES, EXTRACTION_CACHE_TTL_MS, EXTRACTION_CACHE_PRUNE_MAX_AGE_MS, _extractionPruneInFlight = false;
1317
1314
  var init_cache = __esm(() => {
1318
1315
  init_tokens();
1319
1316
  init_logger();
@@ -2205,8 +2202,8 @@ function buildExtractionContext(extraction, forRange) {
2205
2202
  function computeToolCharPercentage(branchEntries) {
2206
2203
  let totalChars = 0;
2207
2204
  let toolChars = 0;
2208
- for (const e of branchEntries) {
2209
- const m = e?.message;
2205
+ for (const raw of branchEntries) {
2206
+ const m = raw?.message;
2210
2207
  if (!m)
2211
2208
  continue;
2212
2209
  let mc = 0;
@@ -2336,6 +2333,9 @@ init_tokens();
2336
2333
  import path6 from "path";
2337
2334
 
2338
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
2339
  function isToolCallBlock(c) {
2340
2340
  return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
2341
2341
  }
@@ -2411,6 +2411,37 @@ function sanitizeSmartCompactDetails(d) {
2411
2411
  return isValidSmartCompactDetails(repaired) ? repaired : null;
2412
2412
  }
2413
2413
 
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);
2438
+ }
2439
+ for (let j = parts.length - 2;j >= 0; j--) {
2440
+ needles.push(parts.slice(j).join("/"));
2441
+ }
2442
+ return needles;
2443
+ }
2444
+
2414
2445
  // src/utils/extraction.ts
2415
2446
  var TRUNCATE_RE = /\u2026\u2702\d+$/;
2416
2447
  function isTruncated(content) {
@@ -2422,18 +2453,33 @@ var READ_TOOL_HINTS = ["read", "view", "open"];
2422
2453
  function hasToolHint(tool, hints) {
2423
2454
  return hints.some((hint) => tool.includes(hint));
2424
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
+ });
2468
+ }
2469
+ return [{ name: b.name, id: b.id, arguments: b.arguments }];
2470
+ }
2425
2471
  function extractText(content) {
2426
2472
  if (typeof content === "string")
2427
2473
  return content;
2428
- if (Array.isArray(content))
2429
- return content.map((b) => {
2430
- if (typeof b === "string")
2431
- return b;
2432
- if (b?.type === "text")
2433
- return b.text ?? "";
2434
- return "";
2435
- }).join("");
2436
- return "";
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("");
2437
2483
  }
2438
2484
  function mediaKind(type, mime) {
2439
2485
  const s = (type + " " + (mime ?? "")).toLowerCase();
@@ -2484,14 +2530,11 @@ function buildToolCallIndex(msgs) {
2484
2530
  idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
2485
2531
  }
2486
2532
  if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2487
- for (let t = 0;t < b.arguments.tool_uses.length; t++) {
2488
- const use = b.arguments.tool_uses[t];
2489
- const recipient = use?.recipient_name ?? "";
2490
- const toolName = recipient.replace(/^functions\./, "");
2491
- const params = use?.parameters ?? {};
2492
- const realId = use?.id ?? undefined;
2533
+ const nested = flattenToolCallBlock(b);
2534
+ for (let t = 0;t < nested.length; t++) {
2535
+ const tool = nested[t];
2493
2536
  const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
2494
- idx.set(realId || syntheticId, { name: toolName, arguments: params, msgIndex: i });
2537
+ idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
2495
2538
  }
2496
2539
  }
2497
2540
  }
@@ -2556,20 +2599,6 @@ function catalogErrors(msgs, _tcIdx) {
2556
2599
  }
2557
2600
  }
2558
2601
  }
2559
- function flattenToolCallBlock(b) {
2560
- if (!isToolCallBlock(b))
2561
- return [];
2562
- if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
2563
- return b.arguments.tool_uses.map((u) => {
2564
- const recipient = u?.recipient_name ?? "";
2565
- return {
2566
- name: recipient.replace(/^functions\./, ""),
2567
- id: u?.id ?? undefined
2568
- };
2569
- });
2570
- }
2571
- return [{ name: b.name, id: b.id }];
2572
- }
2573
2602
  for (const err of errors) {
2574
2603
  for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
2575
2604
  if (msgs[j]?.role === "assistant") {
@@ -2666,33 +2695,24 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2666
2695
  if (m.role === "assistant") {
2667
2696
  const blocks = Array.isArray(m.content) ? m.content : [];
2668
2697
  for (const b of blocks) {
2669
- if (!isToolCallBlock(b))
2670
- continue;
2671
- const nested = b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses) ? b.arguments.tool_uses.map((u) => {
2672
- const recipient = u?.recipient_name ?? "";
2673
- return {
2674
- name: recipient.replace(/^functions\./, ""),
2675
- args: u?.parameters ?? {}
2676
- };
2677
- }) : [{ name: b.name, args: b.arguments }];
2678
- for (const tool of nested) {
2679
- const fp = tool.args?.path ?? tool.args?.file_path;
2680
- if (fp) {
2681
- const fn = path6.basename(fp);
2682
- if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
2683
- brk = true;
2684
- lastFile = fn;
2685
- primaryFile = fp;
2686
- currentPrimaryFile = fp;
2687
- if (tool.name?.includes("write") || tool.name?.includes("edit")) {
2688
- type = "implementation";
2689
- if (currentType !== "implementation")
2690
- currentType = "implementation";
2691
- } else if (tool.name?.includes("read")) {
2692
- type = "review";
2693
- if (currentType === "exploration")
2694
- currentType = "review";
2695
- }
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";
2696
2716
  }
2697
2717
  }
2698
2718
  }
@@ -2759,8 +2779,13 @@ function extractMainGoal(msgs) {
2759
2779
  function extractOpenLoops(msgs, extraction) {
2760
2780
  const loops = [];
2761
2781
  let loopId = 0;
2782
+ const fileNeedles = extraction.modifiedFiles.map((f) => ({
2783
+ path: f.path,
2784
+ needles: buildPathNeedles(f.path)
2785
+ }));
2762
2786
  for (const err of extraction.errors.filter((e) => !e.resolved)) {
2763
- const errFiles = extraction.modifiedFiles.filter((f) => err.message.toLowerCase().includes(f.path.split("/").pop()?.toLowerCase() ?? "__none__")).map((f) => f.path);
2787
+ const errLower = err.message.toLowerCase();
2788
+ const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path7 }) => path7);
2764
2789
  loops.push({
2765
2790
  id: "loop-" + ++loopId,
2766
2791
  type: "bugfix",
@@ -2860,6 +2885,19 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
2860
2885
  };
2861
2886
  }
2862
2887
 
2888
+ // src/infra/session-identity.ts
2889
+ import { randomUUID } from "crypto";
2890
+ var UNRESOLVED_PREFIX = "unresolved:";
2891
+ function resolveSessionId(ctx) {
2892
+ const resolved = ctx.sessionManager?.getSessionId?.();
2893
+ if (typeof resolved === "string" && resolved.length > 0)
2894
+ return resolved;
2895
+ return UNRESOLVED_PREFIX + randomUUID();
2896
+ }
2897
+ function isUnresolvedSessionId(id) {
2898
+ return id.startsWith(UNRESOLVED_PREFIX);
2899
+ }
2900
+
2863
2901
  // src/app/steps/window.ts
2864
2902
  function resolveCompactionWindow(rc) {
2865
2903
  const usage = rc.ctx.getContextUsage();
@@ -2886,7 +2924,7 @@ function resolveCompactionWindow(rc) {
2886
2924
  return null;
2887
2925
  const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
2888
2926
  const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id;
2889
- const sessionId = rc.ctx.sessionManager.getSessionId?.() ?? "unknown";
2927
+ const sessionId = resolveSessionId(rc.ctx);
2890
2928
  const out = rc;
2891
2929
  out.sessionId = sessionId;
2892
2930
  out.branch = branch;
@@ -2906,9 +2944,32 @@ import { convertToLlm } from "@earendil-works/pi-coding-agent";
2906
2944
 
2907
2945
  // src/utils/session-log.ts
2908
2946
  import * as fs4 from "fs";
2947
+ import * as path7 from "path";
2909
2948
  init_logger();
2910
2949
  init_paths();
2911
- import * as path7 from "path";
2950
+
2951
+ // src/utils/lru.ts
2952
+ function lruGet(m, key) {
2953
+ if (!m.has(key))
2954
+ return;
2955
+ const v = m.get(key);
2956
+ m.delete(key);
2957
+ m.set(key, v);
2958
+ return v;
2959
+ }
2960
+ function lruSet(m, key, value, max) {
2961
+ if (m.has(key))
2962
+ m.delete(key);
2963
+ m.set(key, value);
2964
+ while (m.size > max) {
2965
+ const oldest = m.keys().next().value;
2966
+ if (oldest === undefined)
2967
+ break;
2968
+ m.delete(oldest);
2969
+ }
2970
+ }
2971
+
2972
+ // src/utils/session-log.ts
2912
2973
  function getSessionsDir() {
2913
2974
  return sessionsDir();
2914
2975
  }
@@ -2951,42 +3012,47 @@ function* streamJsonlLines(fp, chunkSize = 64 * 1024) {
2951
3012
  }
2952
3013
  }
2953
3014
  var LOG_PATH_CACHE_TTL_MS = 30000;
3015
+ var DEFAULT_CACHE_MAX_ENTRIES = 8;
3016
+ function getMaxEntries() {
3017
+ const raw = process.env.SMART_COMPACT_LOG_CACHE_MAX;
3018
+ if (!raw)
3019
+ return DEFAULT_CACHE_MAX_ENTRIES;
3020
+ const n = Number.parseInt(raw, 10);
3021
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_CACHE_MAX_ENTRIES;
3022
+ }
2954
3023
  var logPathCache = new Map;
2955
3024
  var messageMapCache = new Map;
2956
3025
  function findSessionLogFile(sessionId) {
3026
+ const home2 = process.env.HOME ?? "/tmp";
3027
+ const now = Date.now();
3028
+ const remember = (path8) => {
3029
+ lruSet(logPathCache, sessionId, { path: path8, expiresAt: now + LOG_PATH_CACHE_TTL_MS, home: home2 }, getMaxEntries());
3030
+ return path8;
3031
+ };
2957
3032
  try {
2958
- const home2 = process.env.HOME ?? "/tmp";
2959
- const cached = logPathCache.get(sessionId);
2960
- if (cached && cached.home === home2 && cached.expiresAt > Date.now())
3033
+ const cached = lruGet(logPathCache, sessionId);
3034
+ if (cached && cached.home === home2 && cached.expiresAt > now)
2961
3035
  return cached.path;
2962
3036
  const sessionsDir2 = getSessionsDir();
2963
- if (!fs4.existsSync(sessionsDir2)) {
2964
- logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: home2 });
2965
- return null;
2966
- }
3037
+ if (!fs4.existsSync(sessionsDir2))
3038
+ return remember(null);
2967
3039
  for (const subdir of fs4.readdirSync(sessionsDir2)) {
2968
3040
  const subdirPath = path7.join(sessionsDir2, subdir);
2969
3041
  const stat = fs4.statSync(subdirPath);
2970
3042
  if (!stat.isDirectory())
2971
3043
  continue;
2972
3044
  const exact = path7.join(subdirPath, sessionId + ".jsonl");
2973
- if (fs4.existsSync(exact)) {
2974
- logPathCache.set(sessionId, { path: exact, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: home2 });
2975
- return exact;
2976
- }
3045
+ if (fs4.existsSync(exact))
3046
+ return remember(exact);
2977
3047
  const files = fs4.readdirSync(subdirPath);
2978
3048
  const match = files.find((f) => f.endsWith("_" + sessionId + ".jsonl"));
2979
- if (match) {
2980
- const found = path7.join(subdirPath, match);
2981
- logPathCache.set(sessionId, { path: found, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: home2 });
2982
- return found;
2983
- }
3049
+ if (match)
3050
+ return remember(path7.join(subdirPath, match));
2984
3051
  }
2985
3052
  } catch (e) {
2986
3053
  debug("findSessionLogFile failed", e);
2987
3054
  }
2988
- logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: process.env.HOME ?? "/tmp" });
2989
- return null;
3055
+ return remember(null);
2990
3056
  }
2991
3057
  function normalizeLogMessage(msg) {
2992
3058
  if (!msg || !msg.role)
@@ -3014,7 +3080,7 @@ function readOriginalMessageMap(sessionId) {
3014
3080
  }
3015
3081
  try {
3016
3082
  const stat = fs4.statSync(logPath);
3017
- const cached = messageMapCache.get(sessionId);
3083
+ const cached = lruGet(messageMapCache, sessionId);
3018
3084
  if (cached && cached.logPath === logPath && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
3019
3085
  return cached.map;
3020
3086
  }
@@ -3036,7 +3102,7 @@ function readOriginalMessageMap(sessionId) {
3036
3102
  }
3037
3103
  debug("readOriginalMessageMap: " + map.size + " msgs from " + logPath);
3038
3104
  if (map.size > 0) {
3039
- messageMapCache.set(sessionId, { logPath, mtimeMs: stat.mtimeMs, size: stat.size, map });
3105
+ lruSet(messageMapCache, sessionId, { logPath, mtimeMs: stat.mtimeMs, size: stat.size, map }, getMaxEntries());
3040
3106
  return map;
3041
3107
  }
3042
3108
  return null;
@@ -3201,35 +3267,34 @@ function pruneRedundant(msgs, precomputedTcIdx) {
3201
3267
  keep.delete(statusIndices[i2]);
3202
3268
  reasonMap.set("pi-auto-context status", (reasonMap.get("pi-auto-context status") ?? 0) + 1);
3203
3269
  }
3204
- const kept = msgs.map((m, idx) => {
3205
- if (!keep.has(idx))
3206
- return null;
3207
- if (m.role !== "toolResult")
3208
- return m;
3270
+ const keptIndices = [];
3271
+ const finalMsgs = [];
3272
+ let originalTokens = 0;
3273
+ let prunedTokens = 0;
3274
+ const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
3275
+ for (let idx = 0;idx < msgs.length; idx++) {
3276
+ const m = msgs[idx];
3209
3277
  const text = extractText(m.content);
3210
- if (text.length > MAX_TOOL_OUTPUT_CHARS) {
3211
- const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
3278
+ if (text.length > 0)
3279
+ originalTokens += estimateTokens(text);
3280
+ if (!keep.has(idx))
3281
+ continue;
3282
+ if (m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS) {
3212
3283
  const head = text.slice(0, half);
3213
3284
  const tail = text.slice(-half);
3214
3285
  const truncated = head + `
3215
3286
  ... [truncated ` + (text.length - MAX_TOOL_OUTPUT_CHARS) + ` chars] ...
3216
3287
  ` + tail;
3217
- return { ...m, content: [{ type: "text", text: truncated }] };
3288
+ finalMsgs.push({ ...m, content: [{ type: "text", text: truncated }] });
3289
+ prunedTokens += estimateTokens(truncated);
3290
+ } else {
3291
+ finalMsgs.push(m);
3292
+ if (text.length > 0)
3293
+ prunedTokens += estimateTokens(text);
3218
3294
  }
3219
- return m;
3220
- });
3221
- const keptIndices = [];
3222
- const finalMsgs = [];
3223
- for (let idx = 0;idx < kept.length; idx++) {
3224
- const m = kept[idx];
3225
- if (m === null)
3226
- continue;
3227
3295
  keptIndices.push(idx);
3228
- finalMsgs.push(m);
3229
3296
  }
3230
3297
  const prunedCount = msgs.length - finalMsgs.length;
3231
- const originalTokens = estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
3232
- const prunedTokens = estimateTokens(finalMsgs.map((m) => extractText(m.content)).join(""));
3233
3298
  const reasons = [...reasonMap.entries()].map(([reason, count]) => ({ count, reason }));
3234
3299
  return {
3235
3300
  messages: finalMsgs,
@@ -4378,6 +4443,27 @@ function failedChunkSummary(ch) {
4378
4443
  // src/phases/verify.ts
4379
4444
  init_constants();
4380
4445
  init_cache();
4446
+ init_tokens();
4447
+
4448
+ // src/utils/file-ref-detect.ts
4449
+ var CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|rb|cs|cpp|c|h|hpp|swift|kt|scala|php|css|scss|html|json|yaml|yml|toml|md|mdx|sh|sql|tf|ini|env|lock|gradle|xml)$/i;
4450
+ var VERSION_RE = /^v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/i;
4451
+ var FILE_REF_CANDIDATE_RE = /[\w.\/-]+\.[\w]+/g;
4452
+ function isLikelyFileRef(candidate) {
4453
+ if (VERSION_RE.test(candidate))
4454
+ return false;
4455
+ if (candidate.includes("/")) {
4456
+ const last = candidate.split("/").pop() ?? "";
4457
+ return last.length > 0 && !VERSION_RE.test(last);
4458
+ }
4459
+ return CODE_EXT_RE.test(candidate);
4460
+ }
4461
+ function extractFileRefs(summary) {
4462
+ const candidates = summary.match(FILE_REF_CANDIDATE_RE) ?? [];
4463
+ return candidates.filter(isLikelyFileRef);
4464
+ }
4465
+
4466
+ // src/phases/verify.ts
4381
4467
  init_logger();
4382
4468
 
4383
4469
  // src/domain/summary-schema.ts
@@ -4583,7 +4669,7 @@ function verifySummary(summary, extraction) {
4583
4669
  score -= 10;
4584
4670
  }
4585
4671
  }
4586
- const summaryFileRefs = (summary.match(/[\w.\/-]+\.[\w]+/g) ?? []).filter((p) => p.includes("/") || p.match(/\.(ts|tsx|js|jsx|rs|py|go|java|rb|css|html|json|yaml|yml|toml|md|sh|sql)$/i));
4672
+ const summaryFileRefs = extractFileRefs(summary);
4587
4673
  const knownFiles = new Set([
4588
4674
  ...extraction.modifiedFiles.map((f) => f.path.toLowerCase()),
4589
4675
  ...extraction.readFiles.map((f) => f.toLowerCase())
@@ -4707,10 +4793,12 @@ Current summary:
4707
4793
 
4708
4794
  Return the COMPLETE updated summary with missing items integrated. Keep the same format.`;
4709
4795
  try {
4796
+ const providerCap = getProviderCaps(model.provider).maxOutputTokens;
4797
+ const maxTokens = Math.min(8192, providerCap);
4710
4798
  const resp = await trackedComplete("patch", model, {
4711
4799
  systemPrompt: COMPACT_SYSTEM_PREFIX,
4712
4800
  messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }], timestamp: Date.now() }]
4713
- }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }, services);
4801
+ }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
4714
4802
  const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
4715
4803
  `).trim();
4716
4804
  return patched.startsWith("##") ? patched : summary;
@@ -5150,10 +5238,10 @@ function stagePendingCompaction(rc) {
5150
5238
  firstKeptEntryId: rc.firstKeptId,
5151
5239
  tokensBefore: rc.totalTokens,
5152
5240
  details: rc.details,
5153
- compactionState: rc.compactionState
5241
+ compactionState: rc.compactionState,
5242
+ sessionId: rc.sessionId
5154
5243
  };
5155
- rc.pendingRef.value = pending;
5156
- rc.pendingRef.createdAt = Date.now();
5244
+ rc.pendingRef.set(pending);
5157
5245
  return pending;
5158
5246
  }
5159
5247
  function applyCompaction(rc) {
@@ -5171,8 +5259,7 @@ function applyCompaction(rc) {
5171
5259
  rc.ctx.ui.notify("Applied \u2713", "info");
5172
5260
  },
5173
5261
  onError: (e) => {
5174
- rc.pendingRef.value = null;
5175
- rc.pendingRef.createdAt = 0;
5262
+ rc.pendingRef.clear();
5176
5263
  rc.ctx.ui.notify("Failed: " + e.message, "error");
5177
5264
  }
5178
5265
  });
@@ -5342,8 +5429,7 @@ async function runSmartCompact(opts) {
5342
5429
  return;
5343
5430
  stagePendingCompaction(stated);
5344
5431
  if (stated.cancellation.timedOut) {
5345
- stated.pendingRef.value = null;
5346
- stated.pendingRef.createdAt = 0;
5432
+ stated.pendingRef.clear();
5347
5433
  return;
5348
5434
  }
5349
5435
  runDamageDetection(stated);
@@ -5359,8 +5445,7 @@ async function runSmartCompact(opts) {
5359
5445
  }
5360
5446
  }
5361
5447
  if (stated.cancellation.timedOut) {
5362
- stated.pendingRef.value = null;
5363
- stated.pendingRef.createdAt = 0;
5448
+ stated.pendingRef.clear();
5364
5449
  return;
5365
5450
  }
5366
5451
  applyCompaction(stated);
@@ -5372,12 +5457,13 @@ async function runSmartCompact(opts) {
5372
5457
  clearTimeout(base.cancellation.timeoutId);
5373
5458
  opts.isRunning.value = false;
5374
5459
  if (base.cancellation.timedOut) {
5375
- base.pendingRef.value = null;
5376
- base.pendingRef.createdAt = 0;
5460
+ base.pendingRef.clear();
5377
5461
  }
5378
5462
  const pipelineMs = Date.now() - base.pipelineStart;
5379
5463
  if (base.flags.autoTriggered && !base.cancellation.timedOut) {
5380
- base.ctx.ui.notify("Compaction completed in " + (pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s"), "info");
5464
+ const dur = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
5465
+ const hasPending = base.pendingRef.isPresent();
5466
+ base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : "Smart compact run finished in " + dur, "info");
5381
5467
  }
5382
5468
  }
5383
5469
  }
@@ -5394,7 +5480,73 @@ function duration(rc) {
5394
5480
 
5395
5481
  // src/index.ts
5396
5482
  init_overlays();
5483
+
5484
+ // src/app/pending-slot.ts
5485
+ function createPendingSlot(opts) {
5486
+ const ttlMs = opts.ttlMs;
5487
+ const now = opts.now ?? Date.now;
5488
+ let value = null;
5489
+ let createdAt = 0;
5490
+ return {
5491
+ set(pending) {
5492
+ value = pending;
5493
+ createdAt = now();
5494
+ },
5495
+ consume(ctx) {
5496
+ const snapshotValue = value;
5497
+ const snapshotCreatedAt = createdAt;
5498
+ if (!snapshotValue)
5499
+ return { kind: "empty" };
5500
+ const ageMs = now() - snapshotCreatedAt;
5501
+ if (ageMs > ttlMs) {
5502
+ value = null;
5503
+ createdAt = 0;
5504
+ return { kind: "expired", ageMs };
5505
+ }
5506
+ const currentSessionId = resolveSessionId(ctx);
5507
+ if (snapshotValue.sessionId !== currentSessionId) {
5508
+ value = null;
5509
+ createdAt = 0;
5510
+ return {
5511
+ kind: "mismatch",
5512
+ expected: snapshotValue.sessionId,
5513
+ actual: currentSessionId
5514
+ };
5515
+ }
5516
+ value = null;
5517
+ createdAt = 0;
5518
+ return { kind: "ok", pending: snapshotValue };
5519
+ },
5520
+ clear() {
5521
+ value = null;
5522
+ createdAt = 0;
5523
+ },
5524
+ isPresent() {
5525
+ return value !== null;
5526
+ },
5527
+ peek() {
5528
+ return value;
5529
+ }
5530
+ };
5531
+ }
5532
+
5533
+ // src/index.ts
5397
5534
  init_logger();
5535
+ function unwrapConsumed(result, ctx) {
5536
+ switch (result.kind) {
5537
+ case "ok":
5538
+ return result.pending;
5539
+ case "empty":
5540
+ return null;
5541
+ case "expired":
5542
+ warn("Discarding expired pending smart compaction after " + Math.round(result.ageMs / 1000) + "s");
5543
+ ctx.ui.notify("Expired pending smart compaction discarded", "warning");
5544
+ return null;
5545
+ case "mismatch":
5546
+ warn("Discarding pending smart compaction prepared for a different session (" + result.expected + " vs " + result.actual + ")");
5547
+ return null;
5548
+ }
5549
+ }
5398
5550
  function resolveModelArg(ctx, modelArg) {
5399
5551
  const [p, ...r] = modelArg.split("/");
5400
5552
  return ctx.modelRegistry.find(p, r.join("/"));
@@ -5422,9 +5574,9 @@ function resolveModels(ctx, primary, config) {
5422
5574
  return { segModel, sumModel };
5423
5575
  }
5424
5576
  function smartCompactExtension(pi) {
5425
- const pendingRef = { value: null, createdAt: 0 };
5426
- const isRunning = { value: false };
5427
5577
  const PENDING_TTL_MS = 5 * 60 * 1000;
5578
+ const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
5579
+ const isRunning = { value: false };
5428
5580
  pi.registerCommand("smart-compact", {
5429
5581
  description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
5430
5582
  getArgumentCompletions: (prefix) => {
@@ -5441,7 +5593,8 @@ function smartCompactExtension(pi) {
5441
5593
  if (flags.includes("metrics") || flags.includes("dashboard")) {
5442
5594
  if (flags.includes("dashboard")) {
5443
5595
  const entries = readMetricsLog(200);
5444
- const sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
5596
+ const resolved = resolveSessionId(ctx);
5597
+ const sessionId = isUnresolvedSessionId(resolved) ? "(no session)" : resolved;
5445
5598
  const action = await showMetricsDashboardUI(ctx, {
5446
5599
  entries,
5447
5600
  currentSessionId: sessionId,
@@ -5499,19 +5652,9 @@ function smartCompactExtension(pi) {
5499
5652
  }
5500
5653
  });
5501
5654
  pi.on("session_before_compact", async (_event, ctx) => {
5502
- if (pendingRef.value) {
5503
- const age = Date.now() - pendingRef.createdAt;
5504
- if (age > PENDING_TTL_MS) {
5505
- warn("Discarding expired pending smart compaction after " + Math.round(age / 1000) + "s");
5506
- ctx.ui?.notify?.("Expired pending smart compaction discarded", "warning");
5507
- pendingRef.value = null;
5508
- pendingRef.createdAt = 0;
5509
- } else {
5510
- const c = pendingRef.value;
5511
- pendingRef.value = null;
5512
- pendingRef.createdAt = 0;
5513
- return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
5514
- }
5655
+ const consumed = unwrapConsumed(pendingRef.consume(ctx), ctx);
5656
+ if (consumed) {
5657
+ return { compaction: { summary: consumed.summary, firstKeptEntryId: consumed.firstKeptEntryId, tokensBefore: consumed.tokensBefore, details: consumed.details } };
5515
5658
  }
5516
5659
  const config = loadConfig();
5517
5660
  if (!config.autoTrigger)
@@ -5557,11 +5700,9 @@ function smartCompactExtension(pi) {
5557
5700
  } finally {
5558
5701
  clearTimeout(timeoutId);
5559
5702
  }
5560
- const pending = pendingRef.value;
5561
- if (pending) {
5562
- pendingRef.value = null;
5563
- pendingRef.createdAt = 0;
5564
- return { compaction: { summary: pending.summary, firstKeptEntryId: pending.firstKeptEntryId, tokensBefore: pending.tokensBefore, details: pending.details } };
5703
+ const fresh = unwrapConsumed(pendingRef.consume(ctx), ctx);
5704
+ if (fresh) {
5705
+ return { compaction: { summary: fresh.summary, firstKeptEntryId: fresh.firstKeptEntryId, tokensBefore: fresh.tokensBefore, details: fresh.details } };
5565
5706
  }
5566
5707
  }
5567
5708
  } catch (e) {
@@ -5601,7 +5742,6 @@ Dashboard: ` + fp : "") }], details: undefined };
5601
5742
  }
5602
5743
  const config = loadConfig();
5603
5744
  const resolvedProfile = profile ?? config.profile;
5604
- const cmdCtx = ctx;
5605
5745
  const usage = ctx.getContextUsage?.();
5606
5746
  const totalTokens = usage?.tokens ?? 0;
5607
5747
  const rawPct = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
@@ -5612,17 +5752,18 @@ Dashboard: ` + fp : "") }], details: undefined };
5612
5752
  if (rawPct < config.minContextPercent) {
5613
5753
  return { content: [{ type: "text", text: "Context is only " + pct + "% full (" + totalTokens.toLocaleString() + " tokens). Compaction is not needed yet. The tool=97% in status means tool output ratio, NOT context usage." }], details: undefined };
5614
5754
  }
5615
- const cur = "model" in ctx ? ctx.model : undefined;
5616
- const { segModel, sumModel } = resolveModels(cmdCtx, cur, config);
5755
+ const cur = ctx.model;
5756
+ const { segModel, sumModel } = resolveModels(ctx, cur, config);
5617
5757
  if (!sumModel) {
5618
5758
  return { content: [{ type: "text", text: "Error: Could not resolve model." }], details: undefined };
5619
5759
  }
5620
5760
  try {
5621
5761
  const toolStart = Date.now();
5622
- await runSmartCompact({ ctx: cmdCtx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
5762
+ await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
5623
5763
  const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
5624
- if (pendingRef.value) {
5625
- return { content: [{ type: "text", text: "Smart summary prepared (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? 0).toLocaleString() + " \u2014 summary cached for " + Math.round(PENDING_TTL_MS / 60000) + " min. The next /compact will use this summary automatically." }], details: undefined };
5764
+ const staged = pendingRef.peek();
5765
+ if (staged) {
5766
+ return { content: [{ type: "text", text: "Smart summary prepared (" + resolvedProfile + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 summary cached for " + Math.round(PENDING_TTL_MS / 60000) + " min. The next /compact will use this summary automatically." }], details: undefined };
5626
5767
  }
5627
5768
  return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }], details: undefined };
5628
5769
  } catch (error) {