pi-smart-compact 7.13.1 → 7.14.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.
- package/CHANGELOG.md +43 -0
- package/README.md +2 -8
- package/dist/app/pending-slot.d.ts +88 -0
- package/dist/app/pending-slot.d.ts.map +1 -0
- package/dist/app/run-context.d.ts +12 -10
- package/dist/app/run-context.d.ts.map +1 -1
- package/dist/app/run-smart-compact.d.ts +13 -8
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/steps/persist.d.ts.map +1 -1
- package/dist/app/steps/window.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +291 -150
- package/dist/infra/session-identity.d.ts +54 -0
- package/dist/infra/session-identity.d.ts.map +1 -0
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlays.d.ts +3 -3
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +0 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +22 -0
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/file-needles.d.ts +44 -0
- package/dist/utils/file-needles.d.ts.map +1 -0
- package/dist/utils/file-ref-detect.d.ts +50 -0
- package/dist/utils/file-ref-detect.d.ts.map +1 -0
- package/dist/utils/helpers.d.ts +1 -15
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/lru.d.ts +31 -0
- package/dist/utils/lru.d.ts.map +1 -0
- package/dist/utils/pruning.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts +31 -0
- package/dist/utils/session-log.d.ts.map +1 -1
- package/docs/assets/pi-smart-compact.png +0 -0
- package/package.json +9 -7
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.
|
|
19
|
+
var VERSION = "7.14.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
|
|
852
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
2209
|
-
const m =
|
|
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
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
return
|
|
2435
|
-
|
|
2436
|
-
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
const
|
|
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(
|
|
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
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2959
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
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 >
|
|
3211
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
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
|
|
5561
|
-
if (
|
|
5562
|
-
|
|
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 =
|
|
5616
|
-
const { segModel, sumModel } = resolveModels(
|
|
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
|
|
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
|
-
|
|
5625
|
-
|
|
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) {
|