pi-smart-compact 7.9.4 → 7.10.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 +44 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +508 -103
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +20 -3
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +3 -0
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/fingerprint.d.ts +13 -11
- package/dist/utils/fingerprint.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +24 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/pruning.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts +44 -0
- package/dist/utils/session-log.d.ts.map +1 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/constants.ts
|
|
3
|
-
var VERSION = "7.9.
|
|
3
|
+
var VERSION = "7.9.5";
|
|
4
4
|
var CHARS_PER_TOKEN = 3.8;
|
|
5
5
|
var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
|
|
6
6
|
var PROFILES = {
|
|
@@ -35,6 +35,7 @@ var DEFAULT_CONFIG = {
|
|
|
35
35
|
summaryModel: null,
|
|
36
36
|
segmentationModel: null,
|
|
37
37
|
autoTrigger: true,
|
|
38
|
+
autoTriggerTimeoutMs: 45000,
|
|
38
39
|
backupEnabled: true,
|
|
39
40
|
backupDir: ""
|
|
40
41
|
};
|
|
@@ -191,6 +192,9 @@ function warn(msg, err) {
|
|
|
191
192
|
const detail = err instanceof Error ? err.message : err ?? "";
|
|
192
193
|
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
193
194
|
}
|
|
195
|
+
function info(msg, ...args) {
|
|
196
|
+
console.error(LOG_PREFIX + " [info] " + msg, ...args);
|
|
197
|
+
}
|
|
194
198
|
function debug(msg, ...args) {
|
|
195
199
|
if (DEBUG)
|
|
196
200
|
console.error(LOG_PREFIX + " [debug] " + msg, ...args);
|
|
@@ -223,6 +227,13 @@ function validateSmartCompactConfig(sc) {
|
|
|
223
227
|
warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
224
228
|
delete sc.profiles;
|
|
225
229
|
}
|
|
230
|
+
if ("autoTriggerTimeoutMs" in sc) {
|
|
231
|
+
const v = sc.autoTriggerTimeoutMs;
|
|
232
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
|
|
233
|
+
warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
|
|
234
|
+
delete sc.autoTriggerTimeoutMs;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
226
237
|
}
|
|
227
238
|
var _cfg = null;
|
|
228
239
|
var _cfgMtime = 0;
|
|
@@ -244,7 +255,7 @@ function loadConfig() {
|
|
|
244
255
|
_cfgMtime = stat.mtimeMs;
|
|
245
256
|
return _cfg;
|
|
246
257
|
} catch (e) {
|
|
247
|
-
|
|
258
|
+
debug("loadConfig: settings.json not found or unreadable, using defaults", e);
|
|
248
259
|
const fallback = { ...DEFAULT_CONFIG, backupDir: path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups") };
|
|
249
260
|
_cfg = fallback;
|
|
250
261
|
return fallback;
|
|
@@ -282,11 +293,47 @@ function getPreviousCompactionContext(branch) {
|
|
|
282
293
|
return `
|
|
283
294
|
[IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
|
|
284
295
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
function findLastAnchorIndex(branchEntries) {
|
|
297
|
+
for (let i = branchEntries.length - 1;i >= 0; i--) {
|
|
298
|
+
const e = branchEntries[i];
|
|
299
|
+
if (e?.type !== "message")
|
|
300
|
+
continue;
|
|
301
|
+
const msg = e.message;
|
|
302
|
+
if (msg?.role !== "toolResult")
|
|
303
|
+
continue;
|
|
304
|
+
if (msg?.toolName === "context" && msg?.details?.anchor) {
|
|
305
|
+
return i;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return -1;
|
|
309
|
+
}
|
|
310
|
+
function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
|
|
311
|
+
let msgCount = 0;
|
|
312
|
+
for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
|
|
313
|
+
const e = branchEntries[i];
|
|
314
|
+
if (e?.type === "message") {
|
|
315
|
+
if (msgCount >= msgs.length)
|
|
316
|
+
return msgs.length - 1;
|
|
317
|
+
msgCount++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Math.min(msgCount - 1, msgs.length - 1);
|
|
321
|
+
}
|
|
322
|
+
function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
|
|
323
|
+
let adjusted = keepFromIndex;
|
|
324
|
+
if (branchEntries && branchEntries.length > 0) {
|
|
325
|
+
const lastAnchorBranchIdx = findLastAnchorIndex(branchEntries);
|
|
326
|
+
if (lastAnchorBranchIdx >= 0) {
|
|
327
|
+
const lastAnchorMsgIdx = branchIndexToMsgIndex(branchEntries, lastAnchorBranchIdx, msgs);
|
|
328
|
+
if (adjusted > lastAnchorMsgIdx && lastAnchorMsgIdx >= 0) {
|
|
329
|
+
adjusted = lastAnchorMsgIdx;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (adjusted <= 0 || adjusted >= msgs.length)
|
|
334
|
+
return adjusted;
|
|
335
|
+
const last = msgs[adjusted - 1];
|
|
336
|
+
const first = msgs[adjusted];
|
|
290
337
|
if (last && first) {
|
|
291
338
|
const getText = (msg) => {
|
|
292
339
|
const m = msg;
|
|
@@ -310,9 +357,45 @@ function smartKeepBoundary(msgs, keepFromIndex) {
|
|
|
310
357
|
fileRe.lastIndex = 0;
|
|
311
358
|
const keptFiles = new Set([...keptText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
312
359
|
if ([...lastFiles].filter((f) => keptFiles.has(f)).length > 0)
|
|
313
|
-
return
|
|
360
|
+
return adjusted - 1;
|
|
314
361
|
}
|
|
315
|
-
return
|
|
362
|
+
return adjusted;
|
|
363
|
+
}
|
|
364
|
+
function guardToolCallBoundary(msgs, keepFrom) {
|
|
365
|
+
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
366
|
+
return keepFrom;
|
|
367
|
+
const tcMap = new Map;
|
|
368
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
369
|
+
const m = msgs[i].message;
|
|
370
|
+
if (m?.role !== "assistant")
|
|
371
|
+
continue;
|
|
372
|
+
const blocks = Array.isArray(m?.content) ? m.content : [];
|
|
373
|
+
for (const b of blocks) {
|
|
374
|
+
if (b?.type === "toolCall" && b.id) {
|
|
375
|
+
tcMap.set(b.id, i);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
let adjusted = keepFrom;
|
|
380
|
+
let changed = true;
|
|
381
|
+
while (changed) {
|
|
382
|
+
changed = false;
|
|
383
|
+
for (let i = adjusted;i < msgs.length; i++) {
|
|
384
|
+
const m = msgs[i].message;
|
|
385
|
+
if (m?.role !== "toolResult")
|
|
386
|
+
continue;
|
|
387
|
+
const tcId = m?.toolCallId;
|
|
388
|
+
if (!tcId)
|
|
389
|
+
continue;
|
|
390
|
+
const tcIdx = tcMap.get(tcId);
|
|
391
|
+
if (tcIdx !== undefined && tcIdx < adjusted) {
|
|
392
|
+
adjusted = tcIdx;
|
|
393
|
+
changed = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return Math.max(0, Math.min(adjusted, msgs.length));
|
|
316
399
|
}
|
|
317
400
|
function extractUserNote(args) {
|
|
318
401
|
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
@@ -400,6 +483,39 @@ function buildExtractionContext(extraction, forRange) {
|
|
|
400
483
|
].join(`
|
|
401
484
|
`);
|
|
402
485
|
}
|
|
486
|
+
function computeToolCharPercentage(branchEntries) {
|
|
487
|
+
let totalChars = 0;
|
|
488
|
+
let toolChars = 0;
|
|
489
|
+
for (const e of branchEntries) {
|
|
490
|
+
const m = e?.message;
|
|
491
|
+
if (!m)
|
|
492
|
+
continue;
|
|
493
|
+
let mc = 0;
|
|
494
|
+
if (typeof m.content === "string") {
|
|
495
|
+
mc = m.content.length;
|
|
496
|
+
} else if (Array.isArray(m.content)) {
|
|
497
|
+
for (const part of m.content) {
|
|
498
|
+
if (typeof part?.text === "string")
|
|
499
|
+
mc += part.text.length;
|
|
500
|
+
else if (typeof part?.content === "string")
|
|
501
|
+
mc += part.content.length;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
totalChars += mc;
|
|
505
|
+
if (m.role === "toolResult")
|
|
506
|
+
toolChars += mc;
|
|
507
|
+
}
|
|
508
|
+
return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
|
|
509
|
+
}
|
|
510
|
+
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold) {
|
|
511
|
+
if (totalTokens < minThreshold)
|
|
512
|
+
return "none";
|
|
513
|
+
if (contextPercent < 45 && toolPercent < 60)
|
|
514
|
+
return "none";
|
|
515
|
+
if (contextPercent < 80)
|
|
516
|
+
return "light";
|
|
517
|
+
return "full";
|
|
518
|
+
}
|
|
403
519
|
function inferSessionType(extraction, report) {
|
|
404
520
|
if (report?.sessionType)
|
|
405
521
|
return report.sessionType;
|
|
@@ -588,7 +704,20 @@ function getCompactSessionId() {
|
|
|
588
704
|
function resetCompactSessionId() {
|
|
589
705
|
_compactSessionId = null;
|
|
590
706
|
}
|
|
591
|
-
|
|
707
|
+
var INTERNAL_PHASES = new Set([
|
|
708
|
+
"explore",
|
|
709
|
+
"explore-loop",
|
|
710
|
+
"explore-retry",
|
|
711
|
+
"explore-direct",
|
|
712
|
+
"single-pass",
|
|
713
|
+
"batch",
|
|
714
|
+
"assemble",
|
|
715
|
+
"patch"
|
|
716
|
+
]);
|
|
717
|
+
function cacheOpts(opts, provider, phase) {
|
|
718
|
+
if (phase && INTERNAL_PHASES.has(phase)) {
|
|
719
|
+
return { ...opts, cacheRetention: "none" };
|
|
720
|
+
}
|
|
592
721
|
const strategy = provider ? getProviderCaps(provider).cacheStrategy : "none";
|
|
593
722
|
const retention = strategy === "none" ? "none" : opts.cacheRetention ?? "short";
|
|
594
723
|
if (retention === "none") {
|
|
@@ -625,7 +754,8 @@ function getMetricsSummary() {
|
|
|
625
754
|
async function trackedComplete(phase, model, reqBody, opts) {
|
|
626
755
|
const start = Date.now();
|
|
627
756
|
try {
|
|
628
|
-
const
|
|
757
|
+
const resolvedOpts = cacheOpts(opts, model.provider, phase);
|
|
758
|
+
const resp = await complete(model, reqBody, resolvedOpts);
|
|
629
759
|
const latency = Date.now() - start;
|
|
630
760
|
const usage = resp.usage;
|
|
631
761
|
const inputT = usage?.input ?? 0;
|
|
@@ -665,7 +795,7 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
665
795
|
function getCachePath(sessionId) {
|
|
666
796
|
return path2.join(CACHE_DIR, "compact-extraction-" + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
|
|
667
797
|
}
|
|
668
|
-
function saveCachedExtraction(sessionId, extraction, msgCount) {
|
|
798
|
+
function saveCachedExtraction(sessionId, extraction, msgCount, firstEntryId, lastEntryId) {
|
|
669
799
|
try {
|
|
670
800
|
if (!fs2.existsSync(CACHE_DIR))
|
|
671
801
|
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
@@ -673,7 +803,9 @@ function saveCachedExtraction(sessionId, extraction, msgCount) {
|
|
|
673
803
|
lastMessageIndex: msgCount - 1,
|
|
674
804
|
extraction,
|
|
675
805
|
messageCount: msgCount,
|
|
676
|
-
timestamp: Date.now()
|
|
806
|
+
timestamp: Date.now(),
|
|
807
|
+
firstEntryId,
|
|
808
|
+
lastEntryId
|
|
677
809
|
};
|
|
678
810
|
fs2.writeFileSync(getCachePath(sessionId), JSON.stringify(cached));
|
|
679
811
|
} catch (e) {
|
|
@@ -723,12 +855,18 @@ function mergeExtractions(base, delta, baseMsgCount) {
|
|
|
723
855
|
messageCount: baseMsgCount + delta.messageCount
|
|
724
856
|
};
|
|
725
857
|
}
|
|
726
|
-
function appendMetricsLog(sessionId) {
|
|
858
|
+
function appendMetricsLog(sessionId, extra) {
|
|
727
859
|
try {
|
|
728
860
|
if (!fs2.existsSync(CACHE_DIR))
|
|
729
861
|
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
730
862
|
const logPath = path2.join(CACHE_DIR, "compact-metrics.jsonl");
|
|
731
|
-
const
|
|
863
|
+
const summary = getMetricsSummary();
|
|
864
|
+
const entry = {
|
|
865
|
+
ts: new Date().toISOString(),
|
|
866
|
+
sessionId,
|
|
867
|
+
...summary,
|
|
868
|
+
...extra
|
|
869
|
+
};
|
|
732
870
|
fs2.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
733
871
|
`);
|
|
734
872
|
} catch (e) {
|
|
@@ -755,6 +893,10 @@ function filterToolCalls(content) {
|
|
|
755
893
|
}
|
|
756
894
|
|
|
757
895
|
// src/utils/extraction.ts
|
|
896
|
+
var TRUNCATE_RE = /\u2026\u2702\d+$/;
|
|
897
|
+
function isTruncated(content) {
|
|
898
|
+
return TRUNCATE_RE.test(extractText(content));
|
|
899
|
+
}
|
|
758
900
|
function extractText(content) {
|
|
759
901
|
if (typeof content === "string")
|
|
760
902
|
return content;
|
|
@@ -776,9 +918,22 @@ function buildToolCallIndex(msgs) {
|
|
|
776
918
|
continue;
|
|
777
919
|
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
778
920
|
for (const b of blocks) {
|
|
779
|
-
if (isToolCallBlock(b)
|
|
921
|
+
if (!isToolCallBlock(b))
|
|
922
|
+
continue;
|
|
923
|
+
if (b.id) {
|
|
780
924
|
idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
|
|
781
925
|
}
|
|
926
|
+
if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
|
|
927
|
+
for (let t = 0;t < b.arguments.tool_uses.length; t++) {
|
|
928
|
+
const use = b.arguments.tool_uses[t];
|
|
929
|
+
const recipient = use?.recipient_name ?? "";
|
|
930
|
+
const toolName = recipient.replace(/^functions\./, "");
|
|
931
|
+
const params = use?.parameters ?? {};
|
|
932
|
+
const realId = use?.id ?? undefined;
|
|
933
|
+
const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
|
|
934
|
+
idx.set(realId || syntheticId, { name: toolName, arguments: params, msgIndex: i });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
782
937
|
}
|
|
783
938
|
}
|
|
784
939
|
return idx;
|
|
@@ -802,7 +957,10 @@ function trackFileOps(msgs, _tcIdx) {
|
|
|
802
957
|
const tool = tc.name.toLowerCase();
|
|
803
958
|
if (tool.includes("write") || tool.includes("edit")) {
|
|
804
959
|
const resultText = extractText(m.content);
|
|
805
|
-
if (
|
|
960
|
+
if (isTruncated(resultText)) {
|
|
961
|
+
const existing = modMap.get(filePath);
|
|
962
|
+
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
963
|
+
} else if (!NO_OP_RE.test(resultText)) {
|
|
806
964
|
const existing = modMap.get(filePath);
|
|
807
965
|
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
808
966
|
}
|
|
@@ -832,28 +990,46 @@ function catalogErrors(msgs, _tcIdx) {
|
|
|
832
990
|
}
|
|
833
991
|
if (tc?.name === "bash") {
|
|
834
992
|
const txt = extractText(m.content);
|
|
835
|
-
const isLikelyError = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed)/i.test(txt);
|
|
993
|
+
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);
|
|
836
994
|
if (isLikelyError && txt.length < 2000) {
|
|
837
995
|
errors.push({ index: i, tool: "bash", message: txt.slice(0, 300), retryAttempted: false, resolved: false });
|
|
838
996
|
}
|
|
839
997
|
}
|
|
840
998
|
}
|
|
999
|
+
function flattenToolCallBlock(b) {
|
|
1000
|
+
if (!isToolCallBlock(b))
|
|
1001
|
+
return [];
|
|
1002
|
+
if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
|
|
1003
|
+
return b.arguments.tool_uses.map((u) => {
|
|
1004
|
+
const recipient = u?.recipient_name ?? "";
|
|
1005
|
+
return {
|
|
1006
|
+
name: recipient.replace(/^functions\./, ""),
|
|
1007
|
+
id: u?.id ?? undefined
|
|
1008
|
+
};
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return [{ name: b.name, id: b.id }];
|
|
1012
|
+
}
|
|
841
1013
|
for (const err of errors) {
|
|
842
1014
|
for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
|
|
843
1015
|
if (msgs[j]?.role === "assistant") {
|
|
844
1016
|
const rawBlocks = msgs[j]?.content;
|
|
845
1017
|
const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
|
|
846
1018
|
for (const b of blocks) {
|
|
847
|
-
|
|
848
|
-
err.
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1019
|
+
for (const tool of flattenToolCallBlock(b)) {
|
|
1020
|
+
if (tool.name === err.tool) {
|
|
1021
|
+
err.retryAttempted = true;
|
|
1022
|
+
for (let k = j + 1;k < Math.min(msgs.length, j + 10); k++) {
|
|
1023
|
+
if (msgs[k]?.role === "toolResult" && msgs[k]?.toolCallId === tool.id && !msgs[k]?.isError) {
|
|
1024
|
+
err.resolved = true;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
853
1027
|
}
|
|
1028
|
+
break;
|
|
854
1029
|
}
|
|
855
|
-
break;
|
|
856
1030
|
}
|
|
1031
|
+
if (err.retryAttempted)
|
|
1032
|
+
break;
|
|
857
1033
|
}
|
|
858
1034
|
if (err.retryAttempted)
|
|
859
1035
|
break;
|
|
@@ -917,6 +1093,8 @@ function mineConstraints(msgs) {
|
|
|
917
1093
|
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
918
1094
|
const topics = [];
|
|
919
1095
|
let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
|
|
1096
|
+
let currentType = "exploration";
|
|
1097
|
+
let currentPrimaryFile = null;
|
|
920
1098
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
921
1099
|
for (let i = 0;i < msgs.length; i++) {
|
|
922
1100
|
const m = msgs[i];
|
|
@@ -928,18 +1106,33 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
|
928
1106
|
if (m.role === "assistant") {
|
|
929
1107
|
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
930
1108
|
for (const b of blocks) {
|
|
931
|
-
if (isToolCallBlock(b))
|
|
932
|
-
|
|
1109
|
+
if (!isToolCallBlock(b))
|
|
1110
|
+
continue;
|
|
1111
|
+
const nested = b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses) ? b.arguments.tool_uses.map((u) => {
|
|
1112
|
+
const recipient = u?.recipient_name ?? "";
|
|
1113
|
+
return {
|
|
1114
|
+
name: recipient.replace(/^functions\./, ""),
|
|
1115
|
+
args: u?.parameters ?? {}
|
|
1116
|
+
};
|
|
1117
|
+
}) : [{ name: b.name, args: b.arguments }];
|
|
1118
|
+
for (const tool of nested) {
|
|
1119
|
+
const fp = tool.args?.path ?? tool.args?.file_path;
|
|
933
1120
|
if (fp) {
|
|
934
1121
|
const fn = path3.basename(fp);
|
|
935
1122
|
if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
|
|
936
1123
|
brk = true;
|
|
937
1124
|
lastFile = fn;
|
|
938
1125
|
primaryFile = fp;
|
|
939
|
-
|
|
1126
|
+
currentPrimaryFile = fp;
|
|
1127
|
+
if (tool.name?.includes("write") || tool.name?.includes("edit")) {
|
|
940
1128
|
type = "implementation";
|
|
941
|
-
|
|
1129
|
+
if (currentType !== "implementation")
|
|
1130
|
+
currentType = "implementation";
|
|
1131
|
+
} else if (tool.name?.includes("read")) {
|
|
942
1132
|
type = "review";
|
|
1133
|
+
if (currentType === "exploration")
|
|
1134
|
+
currentType = "review";
|
|
1135
|
+
}
|
|
943
1136
|
}
|
|
944
1137
|
}
|
|
945
1138
|
}
|
|
@@ -947,12 +1140,16 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
|
947
1140
|
if (m.role === "toolResult" && m.isError) {
|
|
948
1141
|
errAcc++;
|
|
949
1142
|
type = "debugging";
|
|
1143
|
+
if (currentType !== "implementation")
|
|
1144
|
+
currentType = "debugging";
|
|
950
1145
|
}
|
|
951
1146
|
if (m.role === "toolResult" && !m.isError) {
|
|
952
1147
|
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
953
1148
|
if (tc?.name === "bash" && /error|fail/i.test(txt)) {
|
|
954
1149
|
errAcc++;
|
|
955
1150
|
type = "debugging";
|
|
1151
|
+
if (currentType !== "implementation")
|
|
1152
|
+
currentType = "debugging";
|
|
956
1153
|
}
|
|
957
1154
|
}
|
|
958
1155
|
if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
|
|
@@ -960,15 +1157,17 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
|
960
1157
|
if (tokenAcc >= pc.maxChunkTokens)
|
|
961
1158
|
brk = true;
|
|
962
1159
|
if (brk && i > startIdx && topics.length < maxSegs - 1) {
|
|
963
|
-
topics.push({ startIndex: startIdx, endIndex: i, primaryFile, type, errorDensity: errAcc });
|
|
1160
|
+
topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
964
1161
|
startIdx = i + 1;
|
|
965
1162
|
tokenAcc = 0;
|
|
966
1163
|
lastFile = null;
|
|
967
1164
|
errAcc = 0;
|
|
1165
|
+
currentType = "exploration";
|
|
1166
|
+
currentPrimaryFile = null;
|
|
968
1167
|
}
|
|
969
1168
|
}
|
|
970
1169
|
if (startIdx < msgs.length) {
|
|
971
|
-
topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile:
|
|
1170
|
+
topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
972
1171
|
}
|
|
973
1172
|
return topics;
|
|
974
1173
|
}
|
|
@@ -1099,18 +1298,127 @@ function extractStructured(msgs, pc) {
|
|
|
1099
1298
|
};
|
|
1100
1299
|
}
|
|
1101
1300
|
|
|
1301
|
+
// src/utils/session-log.ts
|
|
1302
|
+
import * as fs3 from "fs";
|
|
1303
|
+
import * as path4 from "path";
|
|
1304
|
+
function getSessionsDir() {
|
|
1305
|
+
return path4.join(process.env.HOME ?? "/tmp", ".pi", "agent", "sessions");
|
|
1306
|
+
}
|
|
1307
|
+
function findSessionLogFile(sessionId) {
|
|
1308
|
+
try {
|
|
1309
|
+
const sessionsDir = getSessionsDir();
|
|
1310
|
+
if (!fs3.existsSync(sessionsDir))
|
|
1311
|
+
return null;
|
|
1312
|
+
for (const subdir of fs3.readdirSync(sessionsDir)) {
|
|
1313
|
+
const subdirPath = path4.join(sessionsDir, subdir);
|
|
1314
|
+
const stat = fs3.statSync(subdirPath);
|
|
1315
|
+
if (!stat.isDirectory())
|
|
1316
|
+
continue;
|
|
1317
|
+
const exact = path4.join(subdirPath, sessionId + ".jsonl");
|
|
1318
|
+
if (fs3.existsSync(exact))
|
|
1319
|
+
return exact;
|
|
1320
|
+
const files = fs3.readdirSync(subdirPath);
|
|
1321
|
+
const match = files.find((f) => f.endsWith("_" + sessionId + ".jsonl"));
|
|
1322
|
+
if (match)
|
|
1323
|
+
return path4.join(subdirPath, match);
|
|
1324
|
+
}
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
debug("findSessionLogFile failed", e);
|
|
1327
|
+
}
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
function normalizeLogMessage(msg) {
|
|
1331
|
+
if (!msg || !msg.role)
|
|
1332
|
+
return null;
|
|
1333
|
+
const role = msg.role;
|
|
1334
|
+
if (role === "user" || role === "assistant" || role === "toolResult") {
|
|
1335
|
+
return {
|
|
1336
|
+
role,
|
|
1337
|
+
content: msg.content,
|
|
1338
|
+
isError: msg.isError,
|
|
1339
|
+
toolCallId: msg.toolCallId,
|
|
1340
|
+
timestamp: msg.content && typeof msg.content === "object" ? Date.now() : undefined
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
function hasTruncatedMessages(msgs) {
|
|
1346
|
+
return msgs.some((m) => TRUNCATE_RE.test(extractText(m.content)));
|
|
1347
|
+
}
|
|
1348
|
+
function readOriginalMessageMap(sessionId) {
|
|
1349
|
+
const logPath = findSessionLogFile(sessionId);
|
|
1350
|
+
if (!logPath) {
|
|
1351
|
+
debug("Session log not found for " + sessionId);
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
const raw = fs3.readFileSync(logPath, "utf-8");
|
|
1356
|
+
const map = new Map;
|
|
1357
|
+
for (const line of raw.split(`
|
|
1358
|
+
`)) {
|
|
1359
|
+
if (!line.trim())
|
|
1360
|
+
continue;
|
|
1361
|
+
let entry;
|
|
1362
|
+
try {
|
|
1363
|
+
entry = JSON.parse(line);
|
|
1364
|
+
} catch {
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (entry.type === "message" && entry.id && entry.message) {
|
|
1368
|
+
const normalized = normalizeLogMessage(entry.message);
|
|
1369
|
+
if (normalized)
|
|
1370
|
+
map.set(entry.id, normalized);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
debug("readOriginalMessageMap: " + map.size + " msgs from " + logPath);
|
|
1374
|
+
return map.size > 0 ? map : null;
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
debug("readOriginalMessageMap failed", e);
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function entryToLlm(entry) {
|
|
1381
|
+
const msg = entry.message;
|
|
1382
|
+
return {
|
|
1383
|
+
role: msg?.role ?? "user",
|
|
1384
|
+
content: msg?.content,
|
|
1385
|
+
toolCallId: msg?.toolCallId,
|
|
1386
|
+
isError: msg?.isError
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
1390
|
+
const logMap = readOriginalMessageMap(sessionId);
|
|
1391
|
+
if (!logMap)
|
|
1392
|
+
return null;
|
|
1393
|
+
let restoredCount = 0;
|
|
1394
|
+
const result = [];
|
|
1395
|
+
for (const entry of toCompactEntries) {
|
|
1396
|
+
const logMsg = entry.id ? logMap.get(entry.id) : undefined;
|
|
1397
|
+
if (logMsg && !hasTruncatedMessages([logMsg])) {
|
|
1398
|
+
result.push(logMsg);
|
|
1399
|
+
restoredCount++;
|
|
1400
|
+
} else {
|
|
1401
|
+
result.push(entryToLlm(entry));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (restoredCount > 0) {
|
|
1405
|
+
info("Session log recovery: " + restoredCount + "/" + toCompactEntries.length + " messages restored from log");
|
|
1406
|
+
}
|
|
1407
|
+
return result;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1102
1410
|
// src/utils/state.ts
|
|
1103
|
-
import
|
|
1104
|
-
import
|
|
1105
|
-
var STATE_DIR =
|
|
1411
|
+
import fs4 from "fs";
|
|
1412
|
+
import path5 from "path";
|
|
1413
|
+
var STATE_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "states");
|
|
1106
1414
|
function getStatePath(projectId) {
|
|
1107
|
-
return
|
|
1415
|
+
return path5.join(STATE_DIR, projectId + ".json");
|
|
1108
1416
|
}
|
|
1109
1417
|
function saveCompactionState(projectId, state) {
|
|
1110
1418
|
try {
|
|
1111
|
-
if (!
|
|
1112
|
-
|
|
1113
|
-
|
|
1419
|
+
if (!fs4.existsSync(STATE_DIR))
|
|
1420
|
+
fs4.mkdirSync(STATE_DIR, { recursive: true });
|
|
1421
|
+
fs4.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
|
|
1114
1422
|
} catch (e) {
|
|
1115
1423
|
warn("saveCompactionState failed", e);
|
|
1116
1424
|
}
|
|
@@ -1118,14 +1426,14 @@ function saveCompactionState(projectId, state) {
|
|
|
1118
1426
|
function loadCompactionState(projectId) {
|
|
1119
1427
|
try {
|
|
1120
1428
|
const fp = getStatePath(projectId);
|
|
1121
|
-
if (!
|
|
1429
|
+
if (!fs4.existsSync(fp))
|
|
1122
1430
|
return null;
|
|
1123
|
-
const data = JSON.parse(
|
|
1431
|
+
const data = JSON.parse(fs4.readFileSync(fp, "utf8"));
|
|
1124
1432
|
if (data.compactionVersion) {
|
|
1125
1433
|
let updatedAt = data.updatedAt;
|
|
1126
1434
|
if (!updatedAt) {
|
|
1127
1435
|
try {
|
|
1128
|
-
updatedAt =
|
|
1436
|
+
updatedAt = fs4.statSync(fp).mtimeMs;
|
|
1129
1437
|
} catch (e) {
|
|
1130
1438
|
debug("statSync failed for state file", e);
|
|
1131
1439
|
updatedAt = 0;
|
|
@@ -1313,6 +1621,7 @@ function extractCriticalContext(summary) {
|
|
|
1313
1621
|
|
|
1314
1622
|
// src/utils/pruning.ts
|
|
1315
1623
|
var ACK_RE = /^(?:I'?ll |let me |sure|ok[,.]?|got it|i understand|i see|now i|next,? i|alright|great|perfect|sounds good|i can|i will|checking|looking|right away)/i;
|
|
1624
|
+
var PI_STATUS_RE = /^\[pi-auto-context\]/;
|
|
1316
1625
|
var MAX_TOOL_OUTPUT_CHARS = 800;
|
|
1317
1626
|
function pruneRedundant(msgs) {
|
|
1318
1627
|
if (msgs.length < 5)
|
|
@@ -1383,6 +1692,17 @@ function pruneRedundant(msgs) {
|
|
|
1383
1692
|
reasonMap.set("Agent acknowledgments", (reasonMap.get("Agent acknowledgments") ?? 0) + 1);
|
|
1384
1693
|
}
|
|
1385
1694
|
}
|
|
1695
|
+
const statusIndices = [];
|
|
1696
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
1697
|
+
const text = extractText(msgs[idx].content);
|
|
1698
|
+
if (PI_STATUS_RE.test(text)) {
|
|
1699
|
+
statusIndices.push(idx);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
for (let i2 = 0;i2 < statusIndices.length - 1; i2++) {
|
|
1703
|
+
keep.delete(statusIndices[i2]);
|
|
1704
|
+
reasonMap.set("pi-auto-context status", (reasonMap.get("pi-auto-context status") ?? 0) + 1);
|
|
1705
|
+
}
|
|
1386
1706
|
const kept = msgs.map((m, idx) => {
|
|
1387
1707
|
if (!keep.has(idx))
|
|
1388
1708
|
return null;
|
|
@@ -1413,10 +1733,11 @@ function pruneRedundant(msgs) {
|
|
|
1413
1733
|
}
|
|
1414
1734
|
|
|
1415
1735
|
// src/utils/fingerprint.ts
|
|
1416
|
-
import
|
|
1417
|
-
import
|
|
1736
|
+
import fs5 from "fs";
|
|
1737
|
+
import path6 from "path";
|
|
1418
1738
|
import crypto3 from "crypto";
|
|
1419
|
-
|
|
1739
|
+
import { execSync } from "child_process";
|
|
1740
|
+
var FINGERPRINT_DIR = path6.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
|
|
1420
1741
|
var LANG_MAP = {
|
|
1421
1742
|
".ts": "typescript",
|
|
1422
1743
|
".tsx": "typescript",
|
|
@@ -1449,7 +1770,7 @@ var FRAMEWORK_SIGNALS = [
|
|
|
1449
1770
|
{ pattern: /package\.json/i, framework: "node" }
|
|
1450
1771
|
];
|
|
1451
1772
|
function getFingerprintPath(projectId) {
|
|
1452
|
-
return
|
|
1773
|
+
return path6.join(FINGERPRINT_DIR, projectId + ".json");
|
|
1453
1774
|
}
|
|
1454
1775
|
var NOISE_PATH_RE = /(?:node_modules|[/\\]\.pi[/\\]agent|[/\\]\.cache|[/\\]\.npm|[/\\]\.bun|[/\\]\.git[/\\])/;
|
|
1455
1776
|
function isProjectPath(filePath) {
|
|
@@ -1458,6 +1779,14 @@ function isProjectPath(filePath) {
|
|
|
1458
1779
|
function hashProjectId(seed) {
|
|
1459
1780
|
return "proj-" + crypto3.createHash("sha256").update(seed).digest("hex").slice(0, 12);
|
|
1460
1781
|
}
|
|
1782
|
+
function findGitRoot(cwd) {
|
|
1783
|
+
try {
|
|
1784
|
+
const out = execSync("git rev-parse --show-toplevel", { cwd, encoding: "utf-8", timeout: 2000 });
|
|
1785
|
+
return out.trim();
|
|
1786
|
+
} catch {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1461
1790
|
function deriveFromAbsolutePaths(paths) {
|
|
1462
1791
|
const segments = paths.map((p) => p.replace(/^\/+/, "").split("/").filter(Boolean)).filter((s) => s.length >= 3);
|
|
1463
1792
|
if (segments.length < 2)
|
|
@@ -1499,36 +1828,42 @@ function deriveFromRelativePaths(paths) {
|
|
|
1499
1828
|
}
|
|
1500
1829
|
}
|
|
1501
1830
|
const stableDirs = [...dir2Counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([d]) => d).sort();
|
|
1502
|
-
|
|
1503
|
-
return hashProjectId(fingerprint);
|
|
1831
|
+
return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
|
|
1504
1832
|
}
|
|
1505
|
-
function deriveProjectId(extraction) {
|
|
1833
|
+
function deriveProjectId(cwd, extraction, sessionId) {
|
|
1834
|
+
if (cwd && cwd !== "/" && cwd !== process.env.HOME) {
|
|
1835
|
+
return hashProjectId(cwd);
|
|
1836
|
+
}
|
|
1506
1837
|
const allPaths = [
|
|
1507
1838
|
...extraction.modifiedFiles.map((f) => f.path),
|
|
1508
1839
|
...extraction.readFiles
|
|
1509
1840
|
].filter(isProjectPath);
|
|
1510
|
-
if (
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1841
|
+
if (allPaths.length) {
|
|
1842
|
+
const absolutePaths = allPaths.filter((p) => p.startsWith("/"));
|
|
1843
|
+
const relativePaths = allPaths.filter((p) => !p.startsWith("/"));
|
|
1844
|
+
if (absolutePaths.length >= 2) {
|
|
1845
|
+
return deriveFromAbsolutePaths(absolutePaths);
|
|
1846
|
+
}
|
|
1847
|
+
if (relativePaths.length >= 2) {
|
|
1848
|
+
return deriveFromRelativePaths(relativePaths);
|
|
1849
|
+
}
|
|
1850
|
+
return hashProjectId(allPaths.sort().join("|"));
|
|
1516
1851
|
}
|
|
1517
|
-
if (
|
|
1518
|
-
return
|
|
1852
|
+
if (sessionId && sessionId !== "unknown") {
|
|
1853
|
+
return hashProjectId("session-" + sessionId);
|
|
1519
1854
|
}
|
|
1520
|
-
return
|
|
1855
|
+
return "unknown";
|
|
1521
1856
|
}
|
|
1522
1857
|
function detectLanguage(extraction) {
|
|
1523
1858
|
const extCounts = new Map;
|
|
1524
1859
|
for (const f of extraction.modifiedFiles) {
|
|
1525
|
-
const ext =
|
|
1860
|
+
const ext = path6.extname(f.path).toLowerCase();
|
|
1526
1861
|
if (ext && LANG_MAP[ext]) {
|
|
1527
1862
|
extCounts.set(LANG_MAP[ext], (extCounts.get(LANG_MAP[ext]) ?? 0) + 1);
|
|
1528
1863
|
}
|
|
1529
1864
|
}
|
|
1530
1865
|
for (const f of extraction.readFiles) {
|
|
1531
|
-
const ext =
|
|
1866
|
+
const ext = path6.extname(f).toLowerCase();
|
|
1532
1867
|
if (ext && LANG_MAP[ext]) {
|
|
1533
1868
|
extCounts.set(LANG_MAP[ext], (extCounts.get(LANG_MAP[ext]) ?? 0) + 1);
|
|
1534
1869
|
}
|
|
@@ -1559,9 +1894,9 @@ function extractKeyDirs(extraction, maxDirs = 8) {
|
|
|
1559
1894
|
function loadProjectFingerprint(projectId) {
|
|
1560
1895
|
try {
|
|
1561
1896
|
const fp = getFingerprintPath(projectId);
|
|
1562
|
-
if (!
|
|
1897
|
+
if (!fs5.existsSync(fp))
|
|
1563
1898
|
return null;
|
|
1564
|
-
const data = JSON.parse(
|
|
1899
|
+
const data = JSON.parse(fs5.readFileSync(fp, "utf8"));
|
|
1565
1900
|
if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
|
|
1566
1901
|
return null;
|
|
1567
1902
|
return data;
|
|
@@ -1572,8 +1907,8 @@ function loadProjectFingerprint(projectId) {
|
|
|
1572
1907
|
}
|
|
1573
1908
|
function saveProjectFingerprint(projectId, extraction) {
|
|
1574
1909
|
try {
|
|
1575
|
-
if (!
|
|
1576
|
-
|
|
1910
|
+
if (!fs5.existsSync(FINGERPRINT_DIR))
|
|
1911
|
+
fs5.mkdirSync(FINGERPRINT_DIR, { recursive: true });
|
|
1577
1912
|
const existing = loadProjectFingerprint(projectId);
|
|
1578
1913
|
const newKnownFiles = [...new Set([
|
|
1579
1914
|
...existing?.knownFiles ?? [],
|
|
@@ -1589,7 +1924,7 @@ function saveProjectFingerprint(projectId, extraction) {
|
|
|
1589
1924
|
sessionCount: (existing?.sessionCount ?? 0) + 1,
|
|
1590
1925
|
updatedAt: Date.now()
|
|
1591
1926
|
};
|
|
1592
|
-
|
|
1927
|
+
fs5.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
|
|
1593
1928
|
} catch (e) {
|
|
1594
1929
|
warn("saveProjectFingerprint failed", e);
|
|
1595
1930
|
}
|
|
@@ -1607,8 +1942,8 @@ function buildProjectContext(fingerprint) {
|
|
|
1607
1942
|
}
|
|
1608
1943
|
|
|
1609
1944
|
// src/utils/damage.ts
|
|
1610
|
-
import
|
|
1611
|
-
import
|
|
1945
|
+
import fs6 from "fs";
|
|
1946
|
+
import path7 from "path";
|
|
1612
1947
|
var COMPLAINT_PATTERNS = [
|
|
1613
1948
|
/(?: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,
|
|
1614
1949
|
/(?:that'?s? not (?:what I|right)|that'?s? wrong|yanl\u0131\u015F|hay\u0131r de\u011Fil|no that'|that doesn'?t match)/i,
|
|
@@ -1690,10 +2025,10 @@ function detectDamage(postMessages, details) {
|
|
|
1690
2025
|
}
|
|
1691
2026
|
function logDamageReport(sessionId, report, details) {
|
|
1692
2027
|
try {
|
|
1693
|
-
const dir =
|
|
1694
|
-
if (!
|
|
1695
|
-
|
|
1696
|
-
const logPath =
|
|
2028
|
+
const dir = path7.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact");
|
|
2029
|
+
if (!fs6.existsSync(dir))
|
|
2030
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2031
|
+
const logPath = path7.join(dir, "damage-reports.jsonl");
|
|
1697
2032
|
const entry = {
|
|
1698
2033
|
ts: new Date().toISOString(),
|
|
1699
2034
|
sessionId,
|
|
@@ -1704,7 +2039,7 @@ function logDamageReport(sessionId, report, details) {
|
|
|
1704
2039
|
signals: report.signals.length,
|
|
1705
2040
|
summary: report.summary
|
|
1706
2041
|
};
|
|
1707
|
-
|
|
2042
|
+
fs6.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
1708
2043
|
`);
|
|
1709
2044
|
} catch (e) {
|
|
1710
2045
|
warn("logDamageReport failed", e);
|
|
@@ -1946,7 +2281,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1946
2281
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1947
2282
|
messages: [{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() }],
|
|
1948
2283
|
tools: EXPLORATION_TOOLS
|
|
1949
|
-
},
|
|
2284
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, signal });
|
|
1950
2285
|
const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
|
|
1951
2286
|
if (toolCalls.length > 0) {
|
|
1952
2287
|
supportsTools = true;
|
|
@@ -1970,7 +2305,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1970
2305
|
` + EXPLORER_SYSTEM_PROMPT,
|
|
1971
2306
|
messages,
|
|
1972
2307
|
tools: EXPLORATION_TOOLS
|
|
1973
|
-
},
|
|
2308
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, signal });
|
|
1974
2309
|
} catch (err) {
|
|
1975
2310
|
warn("Explore loop error", err);
|
|
1976
2311
|
break;
|
|
@@ -2042,7 +2377,7 @@ User steering: ` + userNote : "");
|
|
|
2042
2377
|
const resp = await trackedComplete("explore-retry", model, {
|
|
2043
2378
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2044
2379
|
messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }], timestamp: Date.now() }]
|
|
2045
|
-
},
|
|
2380
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2046
2381
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
2047
2382
|
return parseExplorationReport(text, llmMessages);
|
|
2048
2383
|
} catch (e) {
|
|
@@ -2080,7 +2415,7 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
2080
2415
|
const resp = await trackedComplete("explore-direct", model, {
|
|
2081
2416
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2082
2417
|
messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }]
|
|
2083
|
-
},
|
|
2418
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2084
2419
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2085
2420
|
`).trim();
|
|
2086
2421
|
return parseExplorationReport(text, llmMessages);
|
|
@@ -2166,7 +2501,7 @@ Session-specific instructions:
|
|
|
2166
2501
|
{ role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
|
|
2167
2502
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2168
2503
|
]
|
|
2169
|
-
},
|
|
2504
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal });
|
|
2170
2505
|
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2171
2506
|
`).trim();
|
|
2172
2507
|
if (!summary.startsWith("##"))
|
|
@@ -2197,7 +2532,7 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
|
2197
2532
|
{ role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }], timestamp: Date.now() },
|
|
2198
2533
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2199
2534
|
]
|
|
2200
|
-
},
|
|
2535
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2201
2536
|
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2202
2537
|
`);
|
|
2203
2538
|
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
@@ -2238,7 +2573,7 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
|
|
|
2238
2573
|
{ role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }], timestamp: Date.now() },
|
|
2239
2574
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2240
2575
|
]
|
|
2241
|
-
},
|
|
2576
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2242
2577
|
return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2243
2578
|
`).trim();
|
|
2244
2579
|
}
|
|
@@ -2450,7 +2785,7 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
|
|
|
2450
2785
|
const resp = await trackedComplete("patch", model, {
|
|
2451
2786
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2452
2787
|
messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }], timestamp: Date.now() }]
|
|
2453
|
-
},
|
|
2788
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal });
|
|
2454
2789
|
const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2455
2790
|
`).trim();
|
|
2456
2791
|
return patched.startsWith("##") ? patched : summary;
|
|
@@ -2463,7 +2798,7 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
|
|
|
2463
2798
|
// src/ui/overlays.ts
|
|
2464
2799
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
2465
2800
|
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
2466
|
-
import
|
|
2801
|
+
import path8 from "path";
|
|
2467
2802
|
function renderContextBar(theme, pct, tokens, barLen = 24) {
|
|
2468
2803
|
const clamped = Math.min(Math.max(pct, 0), 100);
|
|
2469
2804
|
const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
|
|
@@ -2639,7 +2974,7 @@ async function showResultScreen(ctx, details, extraction) {
|
|
|
2639
2974
|
const f = modFiles[i];
|
|
2640
2975
|
const fc = extraction.modifiedFiles.find((e) => e.path === f);
|
|
2641
2976
|
const count = fc ? " (" + fc.toolCalls + "x)" : "";
|
|
2642
|
-
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text",
|
|
2977
|
+
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path8.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
|
|
2643
2978
|
}
|
|
2644
2979
|
if (modFiles.length > maxShow) {
|
|
2645
2980
|
c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
|
|
@@ -2715,6 +3050,8 @@ async function runSmartCompact(opts) {
|
|
|
2715
3050
|
ctx.ui.notify("Model resolve failed", "error");
|
|
2716
3051
|
return;
|
|
2717
3052
|
}
|
|
3053
|
+
let timedOut = false;
|
|
3054
|
+
let timeoutId = null;
|
|
2718
3055
|
try {
|
|
2719
3056
|
const config = loadConfig();
|
|
2720
3057
|
const pc = { ...PROFILES[profile], ...config.profiles?.[profile] ?? {} };
|
|
@@ -2730,17 +3067,22 @@ async function runSmartCompact(opts) {
|
|
|
2730
3067
|
const apiHeaders = auth.headers;
|
|
2731
3068
|
const usage = ctx.getContextUsage();
|
|
2732
3069
|
const totalTokens = usage?.tokens ?? 0;
|
|
2733
|
-
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
2734
|
-
isRunning.value = false;
|
|
2735
|
-
if (!autoTriggered)
|
|
2736
|
-
ctx.ui.notify("Context OK or unknown", "info");
|
|
2737
|
-
return;
|
|
2738
|
-
}
|
|
2739
3070
|
const notify = (msg, type = "info") => {
|
|
2740
3071
|
ctx.ui.notify(msg, type === "success" ? "info" : type);
|
|
2741
3072
|
};
|
|
3073
|
+
const vlog = (msg) => {
|
|
3074
|
+
if (verbose)
|
|
3075
|
+
info(msg);
|
|
3076
|
+
};
|
|
2742
3077
|
const ctrl = new AbortController;
|
|
2743
3078
|
const signal = ctrl.signal;
|
|
3079
|
+
if (autoTriggered && config.autoTriggerTimeoutMs > 0) {
|
|
3080
|
+
timeoutId = setTimeout(() => {
|
|
3081
|
+
timedOut = true;
|
|
3082
|
+
ctrl.abort();
|
|
3083
|
+
notify("Smart compact auto-trigger timed out after " + config.autoTriggerTimeoutMs + "ms, falling back to native compact", "warning");
|
|
3084
|
+
}, config.autoTriggerTimeoutMs);
|
|
3085
|
+
}
|
|
2744
3086
|
const modelLabel = summaryModel.provider + "/" + summaryModel.id;
|
|
2745
3087
|
notify("Smart compact: " + modelLabel + ", " + profile + ", tokens=" + totalTokens, "info");
|
|
2746
3088
|
notify("EESV Compact (" + modelLabel + ", " + profile + ") \u2014 " + (totalTokens ?? 0).toLocaleString() + "t", "info");
|
|
@@ -2761,7 +3103,8 @@ async function runSmartCompact(opts) {
|
|
|
2761
3103
|
break;
|
|
2762
3104
|
}
|
|
2763
3105
|
}
|
|
2764
|
-
keepFrom = smartKeepBoundary(msgs, keepFrom);
|
|
3106
|
+
keepFrom = smartKeepBoundary(msgs, keepFrom, branch);
|
|
3107
|
+
keepFrom = guardToolCallBoundary(msgs, keepFrom);
|
|
2765
3108
|
const toCompact = msgs.slice(0, keepFrom);
|
|
2766
3109
|
if (!toCompact.length) {
|
|
2767
3110
|
isRunning.value = false;
|
|
@@ -2771,30 +3114,55 @@ async function runSmartCompact(opts) {
|
|
|
2771
3114
|
if (!autoTriggered) {
|
|
2772
3115
|
showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
|
|
2773
3116
|
}
|
|
2774
|
-
|
|
3117
|
+
let llmMessages = convertToLlm(toCompact.map((e) => e.message));
|
|
3118
|
+
if (hasTruncatedMessages(llmMessages)) {
|
|
3119
|
+
const sessionId2 = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
3120
|
+
const fromLog = resolveCompactionMessages(sessionId2, toCompact);
|
|
3121
|
+
if (fromLog) {
|
|
3122
|
+
llmMessages = fromLog;
|
|
3123
|
+
notify("Using untruncated session log (" + llmMessages.length + " msgs)", "info");
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const contextPercent = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
|
|
3127
|
+
const toolPercent = computeToolCharPercentage(branch);
|
|
3128
|
+
const tier = selectCompactionTier(contextPercent, toolPercent, totalTokens, MIN_TOKEN_THRESHOLD);
|
|
3129
|
+
if (tier === "none") {
|
|
3130
|
+
isRunning.value = false;
|
|
3131
|
+
if (!autoTriggered)
|
|
3132
|
+
ctx.ui.notify("Context OK (" + Math.round(contextPercent) + "%). pi-toolkit manages context well.", "info");
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const shouldSkipExplore = tier === "light";
|
|
2775
3136
|
const pruning = pruneRedundant(llmMessages);
|
|
2776
3137
|
if (pruning.prunedCount > 0) {
|
|
2777
3138
|
notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
2778
3139
|
}
|
|
2779
|
-
|
|
2780
|
-
const convText = serializeConversation(
|
|
3140
|
+
llmMessages = pruning.messages;
|
|
3141
|
+
const convText = serializeConversation(llmMessages);
|
|
2781
3142
|
const convTokens = estimateTokens(convText);
|
|
2782
3143
|
const sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
2783
3144
|
const backupPath = backupConversation(convText, sessionId);
|
|
2784
3145
|
const prevContext = getPreviousCompactionContext(branch);
|
|
2785
3146
|
const cachedExt = loadCachedExtraction(sessionId);
|
|
2786
3147
|
let extraction;
|
|
2787
|
-
|
|
3148
|
+
const currentFirstId = toCompact[0]?.id;
|
|
3149
|
+
const currentLastId = toCompact[toCompact.length - 1]?.id;
|
|
3150
|
+
const cachedLastMsgId = toCompact[cachedExt?.lastMessageIndex ?? -1]?.id;
|
|
3151
|
+
const idsMatch = cachedExt?.firstEntryId && cachedExt?.lastEntryId && cachedExt.firstEntryId === currentFirstId && cachedExt.lastEntryId === cachedLastMsgId;
|
|
3152
|
+
const cacheUsable = idsMatch && cachedExt.messageCount <= llmMessages.length && cachedExt.lastMessageIndex < llmMessages.length - 1;
|
|
3153
|
+
if (cacheUsable) {
|
|
2788
3154
|
const newMsgs = llmMessages.slice(cachedExt.lastMessageIndex + 1);
|
|
2789
3155
|
const delta = extractStructured(newMsgs, pc);
|
|
2790
3156
|
extraction = mergeExtractions(cachedExt.extraction, delta, cachedExt.messageCount);
|
|
2791
3157
|
notify("Phase 1 Incremental: " + (cachedExt.lastMessageIndex + 1) + " cached + " + newMsgs.length + " new messages", "info");
|
|
3158
|
+
vlog("Incremental extraction \u2014 cached messages: " + cachedExt.messageCount + ", current: " + llmMessages.length);
|
|
2792
3159
|
} else {
|
|
2793
3160
|
extraction = extractStructured(llmMessages, pc);
|
|
2794
3161
|
notify("Phase 1 Full: " + extraction.modifiedFiles.length + " files, " + extraction.errors.length + " errors", "info");
|
|
3162
|
+
vlog("Full extraction \u2014 " + llmMessages.length + " messages, tier=" + tier);
|
|
2795
3163
|
}
|
|
2796
|
-
saveCachedExtraction(sessionId, extraction, llmMessages.length);
|
|
2797
|
-
const projectId = deriveProjectId(extraction);
|
|
3164
|
+
saveCachedExtraction(sessionId, extraction, llmMessages.length, currentFirstId, currentLastId);
|
|
3165
|
+
const projectId = deriveProjectId(findGitRoot(ctx.cwd) ?? ctx.cwd, extraction, sessionId);
|
|
2798
3166
|
const fingerprint = loadProjectFingerprint(projectId);
|
|
2799
3167
|
if (fingerprint) {
|
|
2800
3168
|
notify("Project: " + fingerprint.language + (fingerprint.framework ? "/" + fingerprint.framework : "") + " (" + fingerprint.sessionCount + " sessions)", "info");
|
|
@@ -2807,6 +3175,7 @@ async function runSmartCompact(opts) {
|
|
|
2807
3175
|
let explorationReport = null;
|
|
2808
3176
|
let explorationRounds = 0;
|
|
2809
3177
|
let chunkCount = 0;
|
|
3178
|
+
vlog("Tier=" + tier + " | convTokens=" + convTokens + " | singlePassMax=" + pc.singlePassMaxTokens);
|
|
2810
3179
|
if (convTokens < pc.singlePassMaxTokens) {
|
|
2811
3180
|
if (!autoTriggered)
|
|
2812
3181
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
|
|
@@ -2822,7 +3191,7 @@ async function runSmartCompact(opts) {
|
|
|
2822
3191
|
llmCalls = 0;
|
|
2823
3192
|
}
|
|
2824
3193
|
} else {
|
|
2825
|
-
const needsExploration = shouldExplore(extraction);
|
|
3194
|
+
const needsExploration = !shouldSkipExplore && shouldExplore(extraction);
|
|
2826
3195
|
if (needsExploration) {
|
|
2827
3196
|
if (!autoTriggered)
|
|
2828
3197
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
|
|
@@ -2831,6 +3200,7 @@ async function runSmartCompact(opts) {
|
|
|
2831
3200
|
explorationReport = expResult.report;
|
|
2832
3201
|
explorationRounds = expResult.rounds;
|
|
2833
3202
|
notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
3203
|
+
vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
|
|
2834
3204
|
} catch (err) {
|
|
2835
3205
|
notify("Phase 2 Explore: failed - " + (err instanceof Error ? err.message : String(err)), "warning");
|
|
2836
3206
|
}
|
|
@@ -2868,6 +3238,7 @@ async function runSmartCompact(opts) {
|
|
|
2868
3238
|
const chunks = chunkLlmMessages(llmMessages, boundaries, pc);
|
|
2869
3239
|
chunkCount = chunks.length;
|
|
2870
3240
|
notify("Chunked: " + chunkCount + " chunks", "info");
|
|
3241
|
+
vlog("Chunk topics: " + chunks.map((c) => c.topic + "[" + c.startIndex + "-" + c.endIndex + "]").join(", "));
|
|
2871
3242
|
const batches = createBatches(chunks, pc.batchMaxTokens);
|
|
2872
3243
|
const totalBatches = batches.length;
|
|
2873
3244
|
if (!autoTriggered)
|
|
@@ -2947,6 +3318,7 @@ async function runSmartCompact(opts) {
|
|
|
2947
3318
|
if (!autoTriggered)
|
|
2948
3319
|
showProgressOverlay(ctx, { phase: 4, phaseName: "Verify", detail: "Checking...", model: modelLabel, profile, extraction, explorationRounds });
|
|
2949
3320
|
const verification = verifySummary(finalSummary, extraction);
|
|
3321
|
+
vlog("Verification score=" + verification.score + " ok=" + verification.ok + " gaps=" + verification.gaps.length);
|
|
2950
3322
|
if (!verification.ok) {
|
|
2951
3323
|
if (verification.score < 85) {
|
|
2952
3324
|
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + ", applying deterministic patch", "warning");
|
|
@@ -2973,6 +3345,7 @@ async function runSmartCompact(opts) {
|
|
|
2973
3345
|
const pipelineMs = Date.now() - pipelineStart;
|
|
2974
3346
|
const durationStr = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
|
|
2975
3347
|
notify("Done: " + pipelineInfo + " \u2014 saved " + (tokensSaved ?? 0).toLocaleString() + "t (" + durationStr + ")", "success");
|
|
3348
|
+
vlog("Pipeline complete \u2014 method=" + method + " calls=" + llmCalls + " chunks=" + chunkCount + " tokensSaved=" + tokensSaved);
|
|
2976
3349
|
const openLoops = extractOpenLoops(llmMessages, extraction);
|
|
2977
3350
|
if (openLoops.length > 0) {
|
|
2978
3351
|
notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
@@ -3015,11 +3388,24 @@ async function runSmartCompact(opts) {
|
|
|
3015
3388
|
notify("DRY RUN (" + method + ", " + profile + ") \u2014 " + toCompact.length + " msgs, " + llmCalls + " calls", "info");
|
|
3016
3389
|
return;
|
|
3017
3390
|
}
|
|
3391
|
+
if (timedOut) {
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3018
3394
|
pendingRef.value = { summary: finalSummary, firstKeptEntryId: firstKeptId, tokensBefore: totalTokens, details, compactionState };
|
|
3019
3395
|
pendingRef.createdAt = Date.now();
|
|
3020
3396
|
saveProjectFingerprint(projectId, extraction);
|
|
3021
3397
|
saveCompactionState(projectId, compactionState);
|
|
3022
|
-
appendMetricsLog(sessionId
|
|
3398
|
+
appendMetricsLog(sessionId, {
|
|
3399
|
+
profile,
|
|
3400
|
+
tier,
|
|
3401
|
+
contextPercent: Math.round(contextPercent),
|
|
3402
|
+
toolPercent,
|
|
3403
|
+
tokensBefore: totalTokens,
|
|
3404
|
+
tokensSaved,
|
|
3405
|
+
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3406
|
+
chunkCount: chunkCount || 1,
|
|
3407
|
+
verificationScore: verification.score
|
|
3408
|
+
});
|
|
3023
3409
|
try {
|
|
3024
3410
|
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
|
|
3025
3411
|
if (postCompactMsgs.length > 2) {
|
|
@@ -3049,23 +3435,27 @@ async function runSmartCompact(opts) {
|
|
|
3049
3435
|
notify("Result screen skipped", "info");
|
|
3050
3436
|
}
|
|
3051
3437
|
}
|
|
3052
|
-
if (!skipCompact) {
|
|
3438
|
+
if (!skipCompact && !autoTriggered) {
|
|
3053
3439
|
ctx.compact({
|
|
3054
3440
|
customInstructions: "Use pre-computed smart summary from /smart-compact",
|
|
3055
3441
|
onComplete: () => {
|
|
3056
|
-
|
|
3057
|
-
ctx.ui.notify("Applied \u2713", "info");
|
|
3442
|
+
ctx.ui.notify("Applied \u2713", "info");
|
|
3058
3443
|
},
|
|
3059
3444
|
onError: (e) => {
|
|
3060
|
-
|
|
3061
|
-
ctx.ui.notify("Failed: " + e.message, "error");
|
|
3445
|
+
ctx.ui.notify("Failed: " + e.message, "error");
|
|
3062
3446
|
}
|
|
3063
3447
|
});
|
|
3064
3448
|
}
|
|
3065
3449
|
} finally {
|
|
3450
|
+
if (timeoutId)
|
|
3451
|
+
clearTimeout(timeoutId);
|
|
3066
3452
|
isRunning.value = false;
|
|
3453
|
+
if (timedOut) {
|
|
3454
|
+
pendingRef.value = null;
|
|
3455
|
+
pendingRef.createdAt = 0;
|
|
3456
|
+
}
|
|
3067
3457
|
const pipelineMs = Date.now() - pipelineStart;
|
|
3068
|
-
if (autoTriggered) {
|
|
3458
|
+
if (autoTriggered && !timedOut) {
|
|
3069
3459
|
ctx.ui.notify("Compaction completed in " + (pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s"), "info");
|
|
3070
3460
|
}
|
|
3071
3461
|
}
|
|
@@ -3185,7 +3575,22 @@ function smartCompactExtension(pi) {
|
|
|
3185
3575
|
if (!sumModel)
|
|
3186
3576
|
return;
|
|
3187
3577
|
if (!isRunning.value) {
|
|
3188
|
-
|
|
3578
|
+
const compactPromise = runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true });
|
|
3579
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3580
|
+
setTimeout(() => reject(new Error("smart-compact-timeout")), config.autoTriggerTimeoutMs);
|
|
3581
|
+
});
|
|
3582
|
+
try {
|
|
3583
|
+
await Promise.race([compactPromise, timeoutPromise]);
|
|
3584
|
+
} catch (e) {
|
|
3585
|
+
if (e instanceof Error && e.message === "smart-compact-timeout") {
|
|
3586
|
+
warn("Smart compact auto-trigger hard timeout after " + config.autoTriggerTimeoutMs + "ms");
|
|
3587
|
+
isRunning.value = false;
|
|
3588
|
+
pendingRef.value = null;
|
|
3589
|
+
pendingRef.createdAt = 0;
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
throw e;
|
|
3593
|
+
}
|
|
3189
3594
|
const pending = pendingRef.value;
|
|
3190
3595
|
if (pending) {
|
|
3191
3596
|
pendingRef.value = null;
|