pi-smart-compact 7.7.0 → 7.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +408 -402
- package/dist/constants.d.ts +45 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core.d.ts +27 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +316 -154
- package/dist/phases/explore.d.ts +35 -0
- package/dist/phases/explore.d.ts.map +1 -0
- package/dist/phases/synthesize.d.ts +23 -0
- package/dist/phases/synthesize.d.ts.map +1 -0
- package/dist/phases/verify.d.ts +16 -0
- package/dist/phases/verify.d.ts.map +1 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/overlays.d.ts +29 -0
- package/dist/ui/overlays.d.ts.map +1 -0
- package/dist/utils/cache.d.ts +27 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/damage.d.ts +28 -0
- package/dist/utils/damage.d.ts.map +1 -0
- package/dist/utils/extraction.d.ts +27 -0
- package/dist/utils/extraction.d.ts.map +1 -0
- package/dist/utils/fingerprint.d.ts +32 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/helpers.d.ts +22 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/pruning.d.ts +19 -0
- package/dist/utils/pruning.d.ts.map +1 -0
- package/dist/utils/state.d.ts +62 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/tokens.d.ts +8 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/type-guards.d.ts +26 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/docs/assets/pi-smart-compact.png +0 -0
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/constants.ts
|
|
3
|
-
var VERSION = "7.
|
|
3
|
+
var VERSION = "7.9.1";
|
|
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 = {
|
|
@@ -161,6 +161,11 @@ var SESSION_TYPE_INSTRUCTIONS = {
|
|
|
161
161
|
review: "Focus on: files read, issues found, recommendations, approval status. Prioritize findings over changes. Read-only tool calls = REVIEW, not implementation.",
|
|
162
162
|
discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
|
|
163
163
|
};
|
|
164
|
+
var LOG_PREFIX = "[smart-compact]";
|
|
165
|
+
var MIN_TOKEN_THRESHOLD = 5000;
|
|
166
|
+
var MAX_EXPLORATION_ROUNDS = 8;
|
|
167
|
+
var CONFIG_KEY = "smartCompact";
|
|
168
|
+
var CONFIG_KEY_ALT = "semanticCompact";
|
|
164
169
|
var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
|
|
165
170
|
|
|
166
171
|
` + `Your job:
|
|
@@ -179,6 +184,19 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
|
|
|
179
184
|
import fs from "fs";
|
|
180
185
|
import path from "path";
|
|
181
186
|
import crypto from "crypto";
|
|
187
|
+
|
|
188
|
+
// src/utils/logger.ts
|
|
189
|
+
var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
|
|
190
|
+
function warn(msg, err) {
|
|
191
|
+
const detail = err instanceof Error ? err.message : err ?? "";
|
|
192
|
+
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
193
|
+
}
|
|
194
|
+
function debug(msg, ...args) {
|
|
195
|
+
if (DEBUG)
|
|
196
|
+
console.error(LOG_PREFIX + " [debug] " + msg, ...args);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/utils/helpers.ts
|
|
182
200
|
var _cfg = null;
|
|
183
201
|
var _cfgMtime = 0;
|
|
184
202
|
function loadConfig() {
|
|
@@ -188,7 +206,7 @@ function loadConfig() {
|
|
|
188
206
|
if (_cfg && stat.mtimeMs === _cfgMtime)
|
|
189
207
|
return _cfg;
|
|
190
208
|
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
191
|
-
const sc = raw
|
|
209
|
+
const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
|
|
192
210
|
const merged = { ...DEFAULT_CONFIG, ...sc };
|
|
193
211
|
if (sc.profiles)
|
|
194
212
|
merged.profiles = { ...PROFILES, ...sc.profiles };
|
|
@@ -197,8 +215,11 @@ function loadConfig() {
|
|
|
197
215
|
_cfg = merged;
|
|
198
216
|
_cfgMtime = stat.mtimeMs;
|
|
199
217
|
return _cfg;
|
|
200
|
-
} catch {
|
|
201
|
-
|
|
218
|
+
} catch (e) {
|
|
219
|
+
warn("loadConfig failed, using defaults", e);
|
|
220
|
+
const fallback = { ...DEFAULT_CONFIG, backupDir: path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups") };
|
|
221
|
+
_cfg = fallback;
|
|
222
|
+
return fallback;
|
|
202
223
|
}
|
|
203
224
|
}
|
|
204
225
|
function backupConversation(convText, sessionId) {
|
|
@@ -217,7 +238,8 @@ function backupConversation(convText, sessionId) {
|
|
|
217
238
|
|
|
218
239
|
` + convText);
|
|
219
240
|
return fp;
|
|
220
|
-
} catch {
|
|
241
|
+
} catch (e) {
|
|
242
|
+
warn("backupConversation failed", e);
|
|
221
243
|
return null;
|
|
222
244
|
}
|
|
223
245
|
}
|
|
@@ -238,8 +260,23 @@ function smartKeepBoundary(msgs, keepFromIndex) {
|
|
|
238
260
|
const last = msgs[keepFromIndex - 1];
|
|
239
261
|
const first = msgs[keepFromIndex];
|
|
240
262
|
if (last && first) {
|
|
241
|
-
const
|
|
242
|
-
|
|
263
|
+
const getText = (msg) => {
|
|
264
|
+
const m = msg;
|
|
265
|
+
const c = m?.content;
|
|
266
|
+
if (typeof c === "string")
|
|
267
|
+
return c;
|
|
268
|
+
if (Array.isArray(c))
|
|
269
|
+
return c.map((b) => {
|
|
270
|
+
if (typeof b === "string")
|
|
271
|
+
return b;
|
|
272
|
+
if (typeof b === "object" && b !== null && b.type === "text")
|
|
273
|
+
return b.text ?? "";
|
|
274
|
+
return "";
|
|
275
|
+
}).join("");
|
|
276
|
+
return "";
|
|
277
|
+
};
|
|
278
|
+
const lastText = getText(last.message).toLowerCase();
|
|
279
|
+
const keptText = getText(first.message).toLowerCase();
|
|
243
280
|
const fileRe = /(?:path|file)=["']([^"']+)["']/g;
|
|
244
281
|
const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
245
282
|
fileRe.lastIndex = 0;
|
|
@@ -249,6 +286,12 @@ function smartKeepBoundary(msgs, keepFromIndex) {
|
|
|
249
286
|
}
|
|
250
287
|
return keepFromIndex;
|
|
251
288
|
}
|
|
289
|
+
function extractUserNote(args) {
|
|
290
|
+
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
291
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
292
|
+
const nonFlags = tokens.filter((t) => !t.includes("/") && !SKIP.has(t.toLowerCase()));
|
|
293
|
+
return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
|
|
294
|
+
}
|
|
252
295
|
function createBatches(chunks, maxTokens) {
|
|
253
296
|
const batches = [];
|
|
254
297
|
let batch = [], bt = 0;
|
|
@@ -359,6 +402,42 @@ var PROVIDER_MAP = {
|
|
|
359
402
|
concurrencyLimit: 3,
|
|
360
403
|
cacheStrategy: "anthropic"
|
|
361
404
|
},
|
|
405
|
+
anthropic: {
|
|
406
|
+
maxOutputTokens: 8192,
|
|
407
|
+
supportsTools: true,
|
|
408
|
+
jsonReliability: "high",
|
|
409
|
+
instructionFollowing: "high",
|
|
410
|
+
tokenRatioEstimate: 3.5,
|
|
411
|
+
concurrencyLimit: 3,
|
|
412
|
+
cacheStrategy: "anthropic"
|
|
413
|
+
},
|
|
414
|
+
openai: {
|
|
415
|
+
maxOutputTokens: 16384,
|
|
416
|
+
supportsTools: true,
|
|
417
|
+
jsonReliability: "high",
|
|
418
|
+
instructionFollowing: "high",
|
|
419
|
+
tokenRatioEstimate: 4,
|
|
420
|
+
concurrencyLimit: 5,
|
|
421
|
+
cacheStrategy: "openai"
|
|
422
|
+
},
|
|
423
|
+
google: {
|
|
424
|
+
maxOutputTokens: 8192,
|
|
425
|
+
supportsTools: true,
|
|
426
|
+
jsonReliability: "high",
|
|
427
|
+
instructionFollowing: "high",
|
|
428
|
+
tokenRatioEstimate: 3.8,
|
|
429
|
+
concurrencyLimit: 3,
|
|
430
|
+
cacheStrategy: "openai"
|
|
431
|
+
},
|
|
432
|
+
deepseek: {
|
|
433
|
+
maxOutputTokens: 8192,
|
|
434
|
+
supportsTools: true,
|
|
435
|
+
jsonReliability: "medium",
|
|
436
|
+
instructionFollowing: "medium",
|
|
437
|
+
tokenRatioEstimate: 3.6,
|
|
438
|
+
concurrencyLimit: 2,
|
|
439
|
+
cacheStrategy: "none"
|
|
440
|
+
},
|
|
362
441
|
minimax: {
|
|
363
442
|
maxOutputTokens: 4096,
|
|
364
443
|
supportsTools: "probe",
|
|
@@ -377,26 +456,54 @@ var PROVIDER_MAP = {
|
|
|
377
456
|
concurrencyLimit: 2,
|
|
378
457
|
cacheStrategy: "openai"
|
|
379
458
|
},
|
|
380
|
-
|
|
381
|
-
maxOutputTokens:
|
|
459
|
+
mistral: {
|
|
460
|
+
maxOutputTokens: 8192,
|
|
382
461
|
supportsTools: true,
|
|
383
462
|
jsonReliability: "high",
|
|
384
463
|
instructionFollowing: "high",
|
|
385
|
-
tokenRatioEstimate:
|
|
386
|
-
concurrencyLimit:
|
|
464
|
+
tokenRatioEstimate: 3.5,
|
|
465
|
+
concurrencyLimit: 3,
|
|
387
466
|
cacheStrategy: "openai"
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function getProviderCaps(provider) {
|
|
391
|
-
return PROVIDER_MAP[provider] ?? {
|
|
467
|
+
},
|
|
468
|
+
xai: {
|
|
392
469
|
maxOutputTokens: 8192,
|
|
393
|
-
supportsTools:
|
|
470
|
+
supportsTools: true,
|
|
394
471
|
jsonReliability: "medium",
|
|
395
|
-
instructionFollowing: "
|
|
472
|
+
instructionFollowing: "high",
|
|
396
473
|
tokenRatioEstimate: 3.8,
|
|
397
|
-
concurrencyLimit:
|
|
398
|
-
cacheStrategy: "
|
|
399
|
-
}
|
|
474
|
+
concurrencyLimit: 3,
|
|
475
|
+
cacheStrategy: "openai"
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var PROVIDER_ALIASES = [
|
|
479
|
+
{ pattern: /anthropic/i, provider: "anthropic" },
|
|
480
|
+
{ pattern: /zai/i, provider: "zai-anthropic" },
|
|
481
|
+
{ pattern: /openai/i, provider: "openai" },
|
|
482
|
+
{ pattern: /gpt/i, provider: "openai" },
|
|
483
|
+
{ pattern: /google|gemini/i, provider: "google" },
|
|
484
|
+
{ pattern: /deepseek/i, provider: "deepseek" },
|
|
485
|
+
{ pattern: /minimax/i, provider: "minimax" },
|
|
486
|
+
{ pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
|
|
487
|
+
{ pattern: /mistral/i, provider: "mistral" },
|
|
488
|
+
{ pattern: /xai|grok/i, provider: "xai" }
|
|
489
|
+
];
|
|
490
|
+
var DEFAULT_CAPS = {
|
|
491
|
+
maxOutputTokens: 8192,
|
|
492
|
+
supportsTools: "probe",
|
|
493
|
+
jsonReliability: "medium",
|
|
494
|
+
instructionFollowing: "medium",
|
|
495
|
+
tokenRatioEstimate: 3.8,
|
|
496
|
+
concurrencyLimit: 2,
|
|
497
|
+
cacheStrategy: "none"
|
|
498
|
+
};
|
|
499
|
+
function getProviderCaps(provider) {
|
|
500
|
+
if (PROVIDER_MAP[provider])
|
|
501
|
+
return PROVIDER_MAP[provider];
|
|
502
|
+
for (const { pattern, provider: key } of PROVIDER_ALIASES) {
|
|
503
|
+
if (pattern.test(provider))
|
|
504
|
+
return PROVIDER_MAP[key] ?? DEFAULT_CAPS;
|
|
505
|
+
}
|
|
506
|
+
return DEFAULT_CAPS;
|
|
400
507
|
}
|
|
401
508
|
var _calibrationFactors = new Map;
|
|
402
509
|
function getCalibrationFactor(provider) {
|
|
@@ -435,12 +542,13 @@ function getCompactSessionId() {
|
|
|
435
542
|
function resetCompactSessionId() {
|
|
436
543
|
_compactSessionId = null;
|
|
437
544
|
}
|
|
438
|
-
function cacheOpts(opts) {
|
|
439
|
-
const
|
|
545
|
+
function cacheOpts(opts, provider) {
|
|
546
|
+
const strategy = provider ? getProviderCaps(provider).cacheStrategy : "none";
|
|
547
|
+
const retention = strategy === "none" ? "none" : opts.cacheRetention ?? "short";
|
|
440
548
|
if (retention === "none") {
|
|
441
549
|
return { ...opts, cacheRetention: "none" };
|
|
442
550
|
}
|
|
443
|
-
return { ...opts, sessionId: getCompactSessionId(), cacheRetention:
|
|
551
|
+
return { ...opts, sessionId: getCompactSessionId(), cacheRetention: retention };
|
|
444
552
|
}
|
|
445
553
|
var _metrics = [];
|
|
446
554
|
function resetMetrics() {
|
|
@@ -486,9 +594,13 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
486
594
|
latencyMs: latency,
|
|
487
595
|
success: true
|
|
488
596
|
});
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
597
|
+
try {
|
|
598
|
+
if (inputT > 0 && "messages" in reqBody) {
|
|
599
|
+
const rawText = JSON.stringify(reqBody.messages);
|
|
600
|
+
calibrateFromResponse(estimateTokens(rawText), inputT, model.provider);
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
debug("token calibration failed", e);
|
|
492
604
|
}
|
|
493
605
|
return resp;
|
|
494
606
|
} catch (err) {
|
|
@@ -518,7 +630,9 @@ function saveCachedExtraction(sessionId, extraction, msgCount) {
|
|
|
518
630
|
timestamp: Date.now()
|
|
519
631
|
};
|
|
520
632
|
fs2.writeFileSync(getCachePath(sessionId), JSON.stringify(cached));
|
|
521
|
-
} catch {
|
|
633
|
+
} catch (e) {
|
|
634
|
+
warn("saveCachedExtraction failed", e);
|
|
635
|
+
}
|
|
522
636
|
}
|
|
523
637
|
function loadCachedExtraction(sessionId) {
|
|
524
638
|
try {
|
|
@@ -529,13 +643,14 @@ function loadCachedExtraction(sessionId) {
|
|
|
529
643
|
if (Date.now() - cached.timestamp > 3600000)
|
|
530
644
|
return null;
|
|
531
645
|
return cached;
|
|
532
|
-
} catch {
|
|
646
|
+
} catch (e) {
|
|
647
|
+
warn("loadCachedExtraction failed", e);
|
|
533
648
|
return null;
|
|
534
649
|
}
|
|
535
650
|
}
|
|
536
651
|
function mergeExtractions(base, delta, baseMsgCount) {
|
|
537
652
|
return {
|
|
538
|
-
modifiedFiles: [...base.modifiedFiles, ...delta.modifiedFiles],
|
|
653
|
+
modifiedFiles: [...new Map([...base.modifiedFiles, ...delta.modifiedFiles].map((f) => [f.path, f])).values()],
|
|
539
654
|
readFiles: [...new Set([...base.readFiles, ...delta.readFiles])],
|
|
540
655
|
deletedFiles: [...new Set([...base.deletedFiles, ...delta.deletedFiles])],
|
|
541
656
|
errors: [...base.errors, ...delta.errors],
|
|
@@ -557,13 +672,15 @@ function appendMetricsLog(sessionId) {
|
|
|
557
672
|
const entry = { ts: new Date().toISOString(), sessionId, ...getMetricsSummary() };
|
|
558
673
|
fs2.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
559
674
|
`);
|
|
560
|
-
} catch {
|
|
675
|
+
} catch (e) {
|
|
676
|
+
warn("appendMetricsLog failed", e);
|
|
677
|
+
}
|
|
561
678
|
}
|
|
562
679
|
|
|
563
680
|
// src/utils/extraction.ts
|
|
564
681
|
import path3 from "path";
|
|
565
682
|
|
|
566
|
-
// src/
|
|
683
|
+
// src/utils/type-guards.ts
|
|
567
684
|
function isToolCallBlock(c) {
|
|
568
685
|
return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
|
|
569
686
|
}
|
|
@@ -607,8 +724,8 @@ function buildToolCallIndex(msgs) {
|
|
|
607
724
|
}
|
|
608
725
|
return idx;
|
|
609
726
|
}
|
|
610
|
-
function trackFileOps(msgs) {
|
|
611
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
727
|
+
function trackFileOps(msgs, _tcIdx) {
|
|
728
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
612
729
|
const modMap = new Map;
|
|
613
730
|
const readSet = new Set;
|
|
614
731
|
const delSet = new Set;
|
|
@@ -642,8 +759,8 @@ function trackFileOps(msgs) {
|
|
|
642
759
|
deleted: [...delSet]
|
|
643
760
|
};
|
|
644
761
|
}
|
|
645
|
-
function catalogErrors(msgs) {
|
|
646
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
762
|
+
function catalogErrors(msgs, _tcIdx) {
|
|
763
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
647
764
|
const errors = [];
|
|
648
765
|
for (let i = 0;i < msgs.length; i++) {
|
|
649
766
|
const m = msgs[i];
|
|
@@ -665,7 +782,8 @@ function catalogErrors(msgs) {
|
|
|
665
782
|
for (const err of errors) {
|
|
666
783
|
for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
|
|
667
784
|
if (msgs[j]?.role === "assistant") {
|
|
668
|
-
const
|
|
785
|
+
const rawBlocks = msgs[j]?.content;
|
|
786
|
+
const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
|
|
669
787
|
for (const b of blocks) {
|
|
670
788
|
if (isToolCallBlock(b) && b.name === err.tool) {
|
|
671
789
|
err.retryAttempted = true;
|
|
@@ -685,8 +803,8 @@ function catalogErrors(msgs) {
|
|
|
685
803
|
}
|
|
686
804
|
return errors;
|
|
687
805
|
}
|
|
688
|
-
function extractDecisions(msgs) {
|
|
689
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
806
|
+
function extractDecisions(msgs, _tcIdx) {
|
|
807
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
690
808
|
const decisions = [];
|
|
691
809
|
for (const [id, tc] of tcIdx) {
|
|
692
810
|
if (tc.name !== "ask_user")
|
|
@@ -737,10 +855,10 @@ function mineConstraints(msgs) {
|
|
|
737
855
|
}
|
|
738
856
|
return constraints;
|
|
739
857
|
}
|
|
740
|
-
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20) {
|
|
858
|
+
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
741
859
|
const topics = [];
|
|
742
860
|
let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
|
|
743
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
861
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
744
862
|
for (let i = 0;i < msgs.length; i++) {
|
|
745
863
|
const m = msgs[i];
|
|
746
864
|
const txt = extractText(m.content);
|
|
@@ -896,11 +1014,12 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
896
1014
|
return loops;
|
|
897
1015
|
}
|
|
898
1016
|
function extractStructured(msgs, pc) {
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
const
|
|
1017
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
1018
|
+
const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
|
|
1019
|
+
const errors = catalogErrors(msgs, tcIdx);
|
|
1020
|
+
const decisions = extractDecisions(msgs, tcIdx);
|
|
902
1021
|
const constraints = mineConstraints(msgs);
|
|
903
|
-
const topics = segmentTopicsHeuristic(msgs, pc);
|
|
1022
|
+
const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
|
|
904
1023
|
const timeline = buildTimeline(msgs, errors);
|
|
905
1024
|
const mainGoal = extractMainGoal(msgs);
|
|
906
1025
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
@@ -933,7 +1052,9 @@ function saveCompactionState(projectId, state) {
|
|
|
933
1052
|
if (!fs3.existsSync(STATE_DIR))
|
|
934
1053
|
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
935
1054
|
fs3.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
|
|
936
|
-
} catch {
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
warn("saveCompactionState failed", e);
|
|
1057
|
+
}
|
|
937
1058
|
}
|
|
938
1059
|
function loadCompactionState(projectId) {
|
|
939
1060
|
try {
|
|
@@ -941,9 +1062,22 @@ function loadCompactionState(projectId) {
|
|
|
941
1062
|
if (!fs3.existsSync(fp))
|
|
942
1063
|
return null;
|
|
943
1064
|
const data = JSON.parse(fs3.readFileSync(fp, "utf8"));
|
|
944
|
-
if (data.compactionVersion
|
|
1065
|
+
if (data.compactionVersion) {
|
|
1066
|
+
let updatedAt = data.updatedAt;
|
|
1067
|
+
if (!updatedAt) {
|
|
1068
|
+
try {
|
|
1069
|
+
updatedAt = fs3.statSync(fp).mtimeMs;
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
debug("statSync failed for state file", e);
|
|
1072
|
+
updatedAt = 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
945
1078
|
return data;
|
|
946
|
-
} catch {
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
warn("loadCompactionState failed", e);
|
|
947
1081
|
return null;
|
|
948
1082
|
}
|
|
949
1083
|
}
|
|
@@ -991,7 +1125,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
991
1125
|
nextActions,
|
|
992
1126
|
criticalContext,
|
|
993
1127
|
sessionType: report?.sessionType ?? "implementation",
|
|
994
|
-
compactionVersion: VERSION
|
|
1128
|
+
compactionVersion: VERSION,
|
|
1129
|
+
updatedAt: Date.now()
|
|
995
1130
|
};
|
|
996
1131
|
}
|
|
997
1132
|
function injectOpenLoopsSection(summary, openLoops) {
|
|
@@ -1178,7 +1313,8 @@ function pruneRedundant(msgs) {
|
|
|
1178
1313
|
for (let idx = 0;idx < msgs.length; idx++) {
|
|
1179
1314
|
if (msgs[idx].role !== "assistant")
|
|
1180
1315
|
continue;
|
|
1181
|
-
const
|
|
1316
|
+
const rawBlocks = msgs[idx].content;
|
|
1317
|
+
const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
|
|
1182
1318
|
const hasToolCall = blocks.some((b) => isToolCallBlock(b));
|
|
1183
1319
|
if (hasToolCall)
|
|
1184
1320
|
continue;
|
|
@@ -1220,6 +1356,7 @@ function pruneRedundant(msgs) {
|
|
|
1220
1356
|
// src/utils/fingerprint.ts
|
|
1221
1357
|
import fs4 from "fs";
|
|
1222
1358
|
import path5 from "path";
|
|
1359
|
+
import crypto3 from "crypto";
|
|
1223
1360
|
var FINGERPRINT_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
|
|
1224
1361
|
var LANG_MAP = {
|
|
1225
1362
|
".ts": "typescript",
|
|
@@ -1269,11 +1406,8 @@ function deriveProjectId(extraction) {
|
|
|
1269
1406
|
roots.set(root, (roots.get(root) ?? 0) + 1);
|
|
1270
1407
|
}
|
|
1271
1408
|
const topRoot = [...roots.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
hash = (hash << 5) - hash + topRoot.charCodeAt(i) | 0;
|
|
1275
|
-
}
|
|
1276
|
-
return "proj-" + Math.abs(hash).toString(36);
|
|
1409
|
+
const hash = crypto3.createHash("sha256").update(topRoot).digest("hex").slice(0, 12);
|
|
1410
|
+
return "proj-" + hash;
|
|
1277
1411
|
}
|
|
1278
1412
|
function detectLanguage(extraction) {
|
|
1279
1413
|
const extCounts = new Map;
|
|
@@ -1321,7 +1455,8 @@ function loadProjectFingerprint(projectId) {
|
|
|
1321
1455
|
if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
|
|
1322
1456
|
return null;
|
|
1323
1457
|
return data;
|
|
1324
|
-
} catch {
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
warn("loadProjectFingerprint failed", e);
|
|
1325
1460
|
return null;
|
|
1326
1461
|
}
|
|
1327
1462
|
}
|
|
@@ -1345,7 +1480,9 @@ function saveProjectFingerprint(projectId, extraction) {
|
|
|
1345
1480
|
updatedAt: Date.now()
|
|
1346
1481
|
};
|
|
1347
1482
|
fs4.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
|
|
1348
|
-
} catch {
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
warn("saveProjectFingerprint failed", e);
|
|
1485
|
+
}
|
|
1349
1486
|
}
|
|
1350
1487
|
function buildProjectContext(fingerprint) {
|
|
1351
1488
|
if (!fingerprint)
|
|
@@ -1459,7 +1596,9 @@ function logDamageReport(sessionId, report, details) {
|
|
|
1459
1596
|
};
|
|
1460
1597
|
fs5.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
1461
1598
|
`);
|
|
1462
|
-
} catch {
|
|
1599
|
+
} catch (e) {
|
|
1600
|
+
warn("logDamageReport failed", e);
|
|
1601
|
+
}
|
|
1463
1602
|
}
|
|
1464
1603
|
|
|
1465
1604
|
// src/phases/explore.ts
|
|
@@ -1525,7 +1664,13 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
1525
1664
|
}
|
|
1526
1665
|
case "search_conversation": {
|
|
1527
1666
|
const q = (args.query ?? "").toLowerCase();
|
|
1528
|
-
return JSON.stringify(llmMessages.filter((m) =>
|
|
1667
|
+
return JSON.stringify(llmMessages.filter((m) => {
|
|
1668
|
+
const text = extractText(m?.content).toLowerCase();
|
|
1669
|
+
if (text.includes(q))
|
|
1670
|
+
return true;
|
|
1671
|
+
const tcs = filterToolCalls(m?.content);
|
|
1672
|
+
return tcs.some((tc) => JSON.stringify(tc.arguments).toLowerCase().includes(q));
|
|
1673
|
+
}).slice(0, 10).map((m) => ({
|
|
1529
1674
|
idx: llmMessages.indexOf(m),
|
|
1530
1675
|
role: m?.role,
|
|
1531
1676
|
preview: extractText(m?.content).slice(0, 150)
|
|
@@ -1589,11 +1734,15 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
1589
1734
|
let rawJson = json.slice(s, e + 1);
|
|
1590
1735
|
try {
|
|
1591
1736
|
return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
|
|
1592
|
-
} catch {
|
|
1737
|
+
} catch (e2) {
|
|
1738
|
+
debug("JSON parse attempt 1 failed", e2);
|
|
1739
|
+
}
|
|
1593
1740
|
const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"').replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1594
1741
|
try {
|
|
1595
1742
|
return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
|
|
1596
|
-
} catch {
|
|
1743
|
+
} catch (e2) {
|
|
1744
|
+
debug("JSON parse attempt 2 (cleaned) failed", e2);
|
|
1745
|
+
}
|
|
1597
1746
|
const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
|
|
1598
1747
|
if (boundaryMatch) {
|
|
1599
1748
|
try {
|
|
@@ -1604,7 +1753,9 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
1604
1753
|
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
1605
1754
|
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
1606
1755
|
})) };
|
|
1607
|
-
} catch {
|
|
1756
|
+
} catch (e2) {
|
|
1757
|
+
debug("Boundary JSON parse failed", e2);
|
|
1758
|
+
}
|
|
1608
1759
|
}
|
|
1609
1760
|
return fallbackExplorationReport(llmMessages);
|
|
1610
1761
|
}
|
|
@@ -1641,7 +1792,7 @@ function fallbackExplorationReport(llmMessages) {
|
|
|
1641
1792
|
keyDecisions: []
|
|
1642
1793
|
};
|
|
1643
1794
|
}
|
|
1644
|
-
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds =
|
|
1795
|
+
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify) {
|
|
1645
1796
|
const extractionContext = [
|
|
1646
1797
|
"## Deterministic Extraction (verified facts)",
|
|
1647
1798
|
"Message count: " + extraction.messageCount,
|
|
@@ -1679,13 +1830,13 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1679
1830
|
if (retried.boundaries.length)
|
|
1680
1831
|
return { report: retried, rounds: 1, toolSupported: false };
|
|
1681
1832
|
}
|
|
1682
|
-
return { report: report2, rounds:
|
|
1833
|
+
return { report: report2, rounds: 1, toolSupported: false };
|
|
1683
1834
|
}
|
|
1684
1835
|
const probeResp = await trackedComplete("explore", model, {
|
|
1685
1836
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1686
|
-
messages: [{ role: "user", content: [{ type: "text", text: userContent }] }],
|
|
1837
|
+
messages: [{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() }],
|
|
1687
1838
|
tools: EXPLORATION_TOOLS
|
|
1688
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1839
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
|
|
1689
1840
|
const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
|
|
1690
1841
|
if (toolCalls.length > 0) {
|
|
1691
1842
|
supportsTools = true;
|
|
@@ -1709,9 +1860,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1709
1860
|
` + EXPLORER_SYSTEM_PROMPT,
|
|
1710
1861
|
messages,
|
|
1711
1862
|
tools: EXPLORATION_TOOLS
|
|
1712
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1863
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
|
|
1713
1864
|
} catch (err) {
|
|
1714
|
-
|
|
1865
|
+
warn("Explore loop error", err);
|
|
1715
1866
|
break;
|
|
1716
1867
|
}
|
|
1717
1868
|
const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
|
|
@@ -1749,8 +1900,8 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1749
1900
|
}
|
|
1750
1901
|
return { report: report2, rounds: 1, toolSupported: true };
|
|
1751
1902
|
}
|
|
1752
|
-
} catch {
|
|
1753
|
-
|
|
1903
|
+
} catch (e) {
|
|
1904
|
+
warn("Tool calling probe failed for " + cacheKey, e);
|
|
1754
1905
|
_toolSupportCache.set(cacheKey, { result: false, timestamp: Date.now() });
|
|
1755
1906
|
if (notify)
|
|
1756
1907
|
notify("Tool calling not supported, using direct exploration", "warning");
|
|
@@ -1761,7 +1912,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1761
1912
|
if (retried.boundaries.length)
|
|
1762
1913
|
return { report: retried, rounds: 1, toolSupported: false };
|
|
1763
1914
|
}
|
|
1764
|
-
return { report, rounds:
|
|
1915
|
+
return { report, rounds: 1, toolSupported: supportsTools };
|
|
1765
1916
|
}
|
|
1766
1917
|
async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal) {
|
|
1767
1918
|
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
|
|
@@ -1780,11 +1931,12 @@ User steering: ` + userNote : "");
|
|
|
1780
1931
|
try {
|
|
1781
1932
|
const resp = await trackedComplete("explore-retry", model, {
|
|
1782
1933
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1783
|
-
messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }] }]
|
|
1784
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1934
|
+
messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }], timestamp: Date.now() }]
|
|
1935
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1785
1936
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
1786
1937
|
return parseExplorationReport(text, llmMessages);
|
|
1787
|
-
} catch {
|
|
1938
|
+
} catch (e) {
|
|
1939
|
+
debug("explorationRetry failed", e);
|
|
1788
1940
|
return fallbackExplorationReport(llmMessages);
|
|
1789
1941
|
}
|
|
1790
1942
|
}
|
|
@@ -1817,17 +1969,21 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
1817
1969
|
try {
|
|
1818
1970
|
const resp = await trackedComplete("explore-direct", model, {
|
|
1819
1971
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1820
|
-
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }]
|
|
1821
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1972
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }]
|
|
1973
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1822
1974
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1823
1975
|
`).trim();
|
|
1824
1976
|
return parseExplorationReport(text, llmMessages);
|
|
1825
|
-
} catch {
|
|
1977
|
+
} catch (e) {
|
|
1978
|
+
debug("directExploration failed", e);
|
|
1826
1979
|
return fallbackExplorationReport(llmMessages);
|
|
1827
1980
|
}
|
|
1828
1981
|
}
|
|
1829
1982
|
|
|
1830
1983
|
// src/phases/synthesize.ts
|
|
1984
|
+
function estimateChunkTokens(msgs) {
|
|
1985
|
+
return estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
|
|
1986
|
+
}
|
|
1831
1987
|
function chunkLlmMessages(msgs, boundaries, pc) {
|
|
1832
1988
|
if (!msgs.length)
|
|
1833
1989
|
return [];
|
|
@@ -1835,7 +1991,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1835
1991
|
return [{
|
|
1836
1992
|
startIndex: 0,
|
|
1837
1993
|
endIndex: msgs.length - 1,
|
|
1838
|
-
tokenEstimate:
|
|
1994
|
+
tokenEstimate: estimateChunkTokens(msgs),
|
|
1839
1995
|
topic: "Full conversation",
|
|
1840
1996
|
priority: "normal",
|
|
1841
1997
|
messages: msgs
|
|
@@ -1851,7 +2007,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1851
2007
|
chunks.push({
|
|
1852
2008
|
startIndex: start,
|
|
1853
2009
|
endIndex: end - 1,
|
|
1854
|
-
tokenEstimate:
|
|
2010
|
+
tokenEstimate: estimateChunkTokens(slice),
|
|
1855
2011
|
topic: bp.topic || "Segment " + (chunks.length + 1),
|
|
1856
2012
|
priority: bp.priority,
|
|
1857
2013
|
messages: slice
|
|
@@ -1865,7 +2021,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1865
2021
|
chunks.push({
|
|
1866
2022
|
startIndex: start,
|
|
1867
2023
|
endIndex: msgs.length - 1,
|
|
1868
|
-
tokenEstimate:
|
|
2024
|
+
tokenEstimate: estimateChunkTokens(slice),
|
|
1869
2025
|
topic: lastTopic,
|
|
1870
2026
|
priority: "normal",
|
|
1871
2027
|
messages: slice
|
|
@@ -1897,10 +2053,10 @@ Session-specific instructions:
|
|
|
1897
2053
|
const resp = await trackedComplete("single-pass", model, {
|
|
1898
2054
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1899
2055
|
messages: [
|
|
1900
|
-
{ role: "user", content: [{ type: "text", text: adaptedPrefix }] },
|
|
1901
|
-
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
2056
|
+
{ role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
|
|
2057
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
1902
2058
|
]
|
|
1903
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens:
|
|
2059
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal }, model.provider));
|
|
1904
2060
|
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1905
2061
|
`).trim();
|
|
1906
2062
|
if (!summary.startsWith("##"))
|
|
@@ -1928,10 +2084,10 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
|
1928
2084
|
const resp = await trackedComplete("batch", model, {
|
|
1929
2085
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1930
2086
|
messages: [
|
|
1931
|
-
{ role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }] },
|
|
1932
|
-
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
2087
|
+
{ role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }], timestamp: Date.now() },
|
|
2088
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
1933
2089
|
]
|
|
1934
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
2090
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1935
2091
|
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1936
2092
|
`);
|
|
1937
2093
|
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
@@ -1969,10 +2125,10 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
|
|
|
1969
2125
|
const resp = await trackedComplete("assemble", model, {
|
|
1970
2126
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1971
2127
|
messages: [
|
|
1972
|
-
{ role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }] },
|
|
1973
|
-
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
2128
|
+
{ role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }], timestamp: Date.now() },
|
|
2129
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
1974
2130
|
]
|
|
1975
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget,
|
|
2131
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1976
2132
|
return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1977
2133
|
`).trim();
|
|
1978
2134
|
}
|
|
@@ -2120,10 +2276,14 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2120
2276
|
const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
|
|
2121
2277
|
const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
|
|
2122
2278
|
const otherGaps = gaps.filter((g) => !g.startsWith("Missing modified file:") && !g.startsWith("Missing error:") && !g.startsWith("Missing constraint:") && !g.startsWith("Missing decision:") && !g.startsWith("Potentially fabricated") && !g.startsWith("Inconsistency"));
|
|
2279
|
+
const findSectionInsert = (header) => {
|
|
2280
|
+
const re = new RegExp(header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\n", "i");
|
|
2281
|
+
const m = patched.match(re);
|
|
2282
|
+
return m?.index != null ? m.index + m[0].length : null;
|
|
2283
|
+
};
|
|
2123
2284
|
if (fileGaps.length > 0) {
|
|
2124
|
-
const
|
|
2125
|
-
if (
|
|
2126
|
-
const insertPos = filesSection.index + filesSection[0].length;
|
|
2285
|
+
const insertPos = findSectionInsert("## Files Modified");
|
|
2286
|
+
if (insertPos != null) {
|
|
2127
2287
|
const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
|
|
2128
2288
|
`) + `
|
|
2129
2289
|
`;
|
|
@@ -2131,9 +2291,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2131
2291
|
}
|
|
2132
2292
|
}
|
|
2133
2293
|
if (errorGaps.length > 0) {
|
|
2134
|
-
const
|
|
2135
|
-
if (
|
|
2136
|
-
const insertPos = ctxSection.index + ctxSection[0].length;
|
|
2294
|
+
const insertPos = findSectionInsert("## Critical Context");
|
|
2295
|
+
if (insertPos != null) {
|
|
2137
2296
|
const entries = errorGaps.map((g) => "- " + g).join(`
|
|
2138
2297
|
`) + `
|
|
2139
2298
|
`;
|
|
@@ -2141,9 +2300,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2141
2300
|
}
|
|
2142
2301
|
}
|
|
2143
2302
|
if (constraintGaps.length > 0) {
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
const insertPos = constrSection.index + constrSection[0].length;
|
|
2303
|
+
const insertPos = findSectionInsert("## Constraints & Preferences");
|
|
2304
|
+
if (insertPos != null) {
|
|
2147
2305
|
const entries = constraintGaps.map((g) => "- " + g).join(`
|
|
2148
2306
|
`) + `
|
|
2149
2307
|
`;
|
|
@@ -2151,9 +2309,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2151
2309
|
}
|
|
2152
2310
|
}
|
|
2153
2311
|
if (decisionGaps.length > 0) {
|
|
2154
|
-
const
|
|
2155
|
-
if (
|
|
2156
|
-
const insertPos = decSection.index + decSection[0].length;
|
|
2312
|
+
const insertPos = findSectionInsert("## Key Decisions");
|
|
2313
|
+
if (insertPos != null) {
|
|
2157
2314
|
const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
|
|
2158
2315
|
`) + `
|
|
2159
2316
|
`;
|
|
@@ -2182,12 +2339,13 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
|
|
|
2182
2339
|
try {
|
|
2183
2340
|
const resp = await trackedComplete("patch", model, {
|
|
2184
2341
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2185
|
-
messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }] }]
|
|
2342
|
+
messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }], timestamp: Date.now() }]
|
|
2186
2343
|
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
|
|
2187
2344
|
const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2188
2345
|
`).trim();
|
|
2189
2346
|
return patched.startsWith("##") ? patched : summary;
|
|
2190
|
-
} catch {
|
|
2347
|
+
} catch (e) {
|
|
2348
|
+
debug("patchSummary LLM failed", e);
|
|
2191
2349
|
return summary;
|
|
2192
2350
|
}
|
|
2193
2351
|
}
|
|
@@ -2201,7 +2359,8 @@ function renderContextBar(theme, pct, tokens, barLen = 24) {
|
|
|
2201
2359
|
const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
|
|
2202
2360
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
2203
2361
|
const color = clamped > 80 ? "error" : clamped > 50 ? "warning" : "success";
|
|
2204
|
-
|
|
2362
|
+
const fg = theme.fg;
|
|
2363
|
+
return fg("text", " Context: ") + fg(color, bar) + fg("text", " " + clamped + "%") + fg("dim", " (" + (tokens ?? 0).toLocaleString() + "t)");
|
|
2205
2364
|
}
|
|
2206
2365
|
function renderTokenBar(theme, before, after, label, barLen = 30) {
|
|
2207
2366
|
const ratio = before > 0 ? after / before : 0;
|
|
@@ -2211,7 +2370,6 @@ function renderTokenBar(theme, before, after, label, barLen = 30) {
|
|
|
2211
2370
|
const savedColor = savedPct >= 50 ? "success" : savedPct >= 25 ? "warning" : "error";
|
|
2212
2371
|
return theme.fg("text", " " + label + ": ") + theme.fg(savedColor, bar) + theme.fg("text", " " + (after ?? 0).toLocaleString() + "t") + theme.fg(savedColor, " (saved " + savedPct + "%)");
|
|
2213
2372
|
}
|
|
2214
|
-
var _toolSupportCache2 = new Map;
|
|
2215
2373
|
async function selectModel(ctx, opts) {
|
|
2216
2374
|
const available = ctx.modelRegistry.getAvailable();
|
|
2217
2375
|
const options = available.map((m) => ({
|
|
@@ -2243,7 +2401,7 @@ async function selectModel(ctx, opts) {
|
|
|
2243
2401
|
scrollInfo: (t) => theme.fg("dim", t),
|
|
2244
2402
|
noMatch: (t) => theme.fg("warning", t)
|
|
2245
2403
|
});
|
|
2246
|
-
sel.
|
|
2404
|
+
sel.setSelectedIndex(opts.defaultModelIndex);
|
|
2247
2405
|
sel.onSelect = (item) => done(item.value);
|
|
2248
2406
|
sel.onCancel = () => done(null);
|
|
2249
2407
|
c.addChild(sel);
|
|
@@ -2292,7 +2450,7 @@ async function selectProfile(ctx, selectedModel, opts) {
|
|
|
2292
2450
|
scrollInfo: (t) => theme.fg("dim", t),
|
|
2293
2451
|
noMatch: (t) => theme.fg("warning", t)
|
|
2294
2452
|
});
|
|
2295
|
-
sel.
|
|
2453
|
+
sel.setSelectedIndex(1);
|
|
2296
2454
|
sel.onSelect = (item) => done(item.value);
|
|
2297
2455
|
sel.onCancel = () => done(null);
|
|
2298
2456
|
c.addChild(sel);
|
|
@@ -2317,8 +2475,7 @@ function showProgressOverlay(ctx, state) {
|
|
|
2317
2475
|
const progress = Math.round(state.phase / 4 * 100);
|
|
2318
2476
|
const name = phaseNames[state.phase - 1] ?? "?";
|
|
2319
2477
|
const detail = state.detail ? " (" + state.detail + ")" : "";
|
|
2320
|
-
|
|
2321
|
-
ctx.ui.notify("EESV [" + progress + "%] Phase " + state.phase + "/4: " + name + detail, type);
|
|
2478
|
+
ctx.ui.notify("EESV [" + progress + "%] Phase " + state.phase + "/4: " + name + detail, state.phase >= 4 ? "info" : "info");
|
|
2322
2479
|
}
|
|
2323
2480
|
async function showResultScreen(ctx, details, extraction) {
|
|
2324
2481
|
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
@@ -2332,7 +2489,8 @@ async function showResultScreen(ctx, details, extraction) {
|
|
|
2332
2489
|
c.addChild(new Text("", 0, 0));
|
|
2333
2490
|
const methodColors = { eesv: "accent", "single-pass": "success", heuristic: "warning" };
|
|
2334
2491
|
const methodColor = methodColors[details.method] ?? "text";
|
|
2335
|
-
|
|
2492
|
+
const fg = theme.fg;
|
|
2493
|
+
c.addChild(new Text(theme.fg("text", " Method: ") + fg(methodColor, details.method.toUpperCase()) + theme.fg("dim", " \u2022 " + details.llmCalls + " LLM call(s) \u2022 Profile: " + details.profile), 0, 0));
|
|
2336
2494
|
if (details.model) {
|
|
2337
2495
|
c.addChild(new Text(theme.fg("dim", " Model: " + details.model), 0, 0));
|
|
2338
2496
|
}
|
|
@@ -2435,7 +2593,8 @@ async function showCompactUI(ctx, opts) {
|
|
|
2435
2593
|
}
|
|
2436
2594
|
|
|
2437
2595
|
// src/core.ts
|
|
2438
|
-
async function runSmartCompact(
|
|
2596
|
+
async function runSmartCompact(opts) {
|
|
2597
|
+
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
|
|
2439
2598
|
if (isRunning.value)
|
|
2440
2599
|
return;
|
|
2441
2600
|
isRunning.value = true;
|
|
@@ -2459,16 +2618,18 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2459
2618
|
ctx.ui.notify("Auth failed", "error");
|
|
2460
2619
|
return;
|
|
2461
2620
|
}
|
|
2621
|
+
const apiKey = auth.apiKey;
|
|
2622
|
+
const apiHeaders = auth.headers;
|
|
2462
2623
|
const usage = ctx.getContextUsage();
|
|
2463
2624
|
const totalTokens = usage?.tokens ?? 0;
|
|
2464
|
-
if (!totalTokens || totalTokens <
|
|
2625
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
2465
2626
|
isRunning.value = false;
|
|
2466
2627
|
if (!autoTriggered)
|
|
2467
2628
|
ctx.ui.notify("Context OK or unknown", "info");
|
|
2468
2629
|
return;
|
|
2469
2630
|
}
|
|
2470
2631
|
const notify = (msg, type = "info") => {
|
|
2471
|
-
ctx.ui.notify(msg, type);
|
|
2632
|
+
ctx.ui.notify(msg, type === "success" ? "info" : type);
|
|
2472
2633
|
};
|
|
2473
2634
|
const ctrl = new AbortController;
|
|
2474
2635
|
const signal = ctrl.signal;
|
|
@@ -2498,7 +2659,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2498
2659
|
isRunning.value = false;
|
|
2499
2660
|
return;
|
|
2500
2661
|
}
|
|
2501
|
-
const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id
|
|
2662
|
+
const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id;
|
|
2502
2663
|
if (!autoTriggered) {
|
|
2503
2664
|
showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
|
|
2504
2665
|
}
|
|
@@ -2542,7 +2703,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2542
2703
|
if (!autoTriggered)
|
|
2543
2704
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
|
|
2544
2705
|
try {
|
|
2545
|
-
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey
|
|
2706
|
+
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, signal);
|
|
2546
2707
|
finalSummary = r.summary;
|
|
2547
2708
|
method = "single-pass";
|
|
2548
2709
|
llmCalls = r.llmCalls;
|
|
@@ -2558,7 +2719,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2558
2719
|
if (!autoTriggered)
|
|
2559
2720
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
|
|
2560
2721
|
try {
|
|
2561
|
-
const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal,
|
|
2722
|
+
const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, MAX_EXPLORATION_ROUNDS, notify);
|
|
2562
2723
|
explorationReport = expResult.report;
|
|
2563
2724
|
explorationRounds = expResult.rounds;
|
|
2564
2725
|
notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
@@ -2607,7 +2768,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2607
2768
|
const concurrency = caps.concurrencyLimit;
|
|
2608
2769
|
if (totalBatches <= 1) {
|
|
2609
2770
|
try {
|
|
2610
|
-
summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey
|
|
2771
|
+
summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey, headers: apiHeaders }, signal));
|
|
2611
2772
|
} catch (err) {
|
|
2612
2773
|
summaries.push(...batches[0].map((ch) => ({
|
|
2613
2774
|
topic: ch.topic,
|
|
@@ -2630,7 +2791,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2630
2791
|
const wavePromises = waveBatches.map(async (batch, i) => {
|
|
2631
2792
|
const idx = wave + i;
|
|
2632
2793
|
try {
|
|
2633
|
-
results[idx] = await summarizeBatch(batch, extraction, summaryModel, { apiKey
|
|
2794
|
+
results[idx] = await summarizeBatch(batch, extraction, summaryModel, { apiKey, headers: apiHeaders }, signal);
|
|
2634
2795
|
} catch (err) {
|
|
2635
2796
|
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
2636
2797
|
results[idx] = batch.map((ch) => ({
|
|
@@ -2662,13 +2823,13 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2662
2823
|
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "Assembling...", model: modelLabel, profile, extraction, totalBatches: batches.length });
|
|
2663
2824
|
let assemblyCalls = 1;
|
|
2664
2825
|
try {
|
|
2665
|
-
const r = await assembleLLM(summaries, extraction, explorationReport, summaryModel, { apiKey
|
|
2826
|
+
const r = await assembleLLM(summaries, extraction, explorationReport, summaryModel, { apiKey, headers: apiHeaders }, pc.summaryBudgetTokens, prevContext, signal);
|
|
2666
2827
|
if (r?.startsWith("##"))
|
|
2667
2828
|
finalSummary = r;
|
|
2668
2829
|
else
|
|
2669
2830
|
throw new Error("bad");
|
|
2670
2831
|
} catch (err) {
|
|
2671
|
-
|
|
2832
|
+
warn("Assembly failed", err);
|
|
2672
2833
|
finalSummary = assembleFallback(summaries, extraction);
|
|
2673
2834
|
assemblyCalls = 0;
|
|
2674
2835
|
}
|
|
@@ -2686,10 +2847,10 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2686
2847
|
if (!recheck.ok && recheck.score < 75) {
|
|
2687
2848
|
notify("Phase 4 Verify: deterministic patch insufficient (score=" + recheck.score + "), trying LLM patch", "warning");
|
|
2688
2849
|
try {
|
|
2689
|
-
finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey
|
|
2850
|
+
finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey, headers: apiHeaders }, signal);
|
|
2690
2851
|
llmCalls++;
|
|
2691
2852
|
} catch (err) {
|
|
2692
|
-
|
|
2853
|
+
warn("LLM patch failed", err);
|
|
2693
2854
|
}
|
|
2694
2855
|
}
|
|
2695
2856
|
} else {
|
|
@@ -2752,7 +2913,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2752
2913
|
saveCompactionState(projectId, compactionState);
|
|
2753
2914
|
appendMetricsLog(sessionId);
|
|
2754
2915
|
try {
|
|
2755
|
-
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat()
|
|
2916
|
+
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
|
|
2756
2917
|
if (postCompactMsgs.length > 2) {
|
|
2757
2918
|
const lastCompaction = branch.filter((e) => e.type === "compaction").slice(-1)[0];
|
|
2758
2919
|
if (lastCompaction?.details) {
|
|
@@ -2765,7 +2926,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2765
2926
|
}
|
|
2766
2927
|
}
|
|
2767
2928
|
} catch (err) {
|
|
2768
|
-
|
|
2929
|
+
warn("Damage detection error", err);
|
|
2769
2930
|
}
|
|
2770
2931
|
const ms = getMetricsSummary();
|
|
2771
2932
|
if (ms.totalCalls > 0) {
|
|
@@ -2776,7 +2937,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2776
2937
|
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
2777
2938
|
await Promise.race([showResultScreen(ctx, details, extraction), timeout]);
|
|
2778
2939
|
} catch (err) {
|
|
2779
|
-
|
|
2940
|
+
warn("Result screen error", err);
|
|
2780
2941
|
notify("Result screen skipped", "info");
|
|
2781
2942
|
}
|
|
2782
2943
|
}
|
|
@@ -2785,7 +2946,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2785
2946
|
customInstructions: "Use pre-computed smart summary from /smart-compact",
|
|
2786
2947
|
onComplete: () => {
|
|
2787
2948
|
if (!autoTriggered)
|
|
2788
|
-
ctx.ui.notify("Applied \u2713", "
|
|
2949
|
+
ctx.ui.notify("Applied \u2713", "info");
|
|
2789
2950
|
},
|
|
2790
2951
|
onError: (e) => {
|
|
2791
2952
|
if (!autoTriggered)
|
|
@@ -2840,6 +3001,7 @@ function smartCompactExtension(pi) {
|
|
|
2840
3001
|
return m.length ? m : null;
|
|
2841
3002
|
},
|
|
2842
3003
|
handler: async (args, ctx) => {
|
|
3004
|
+
await ctx.waitForIdle();
|
|
2843
3005
|
try {
|
|
2844
3006
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2845
3007
|
const flags = tokens.map((t) => t.toLowerCase());
|
|
@@ -2852,7 +3014,7 @@ function smartCompactExtension(pi) {
|
|
|
2852
3014
|
const usage = ctx.getContextUsage();
|
|
2853
3015
|
const totalTokens = usage?.tokens ?? 0;
|
|
2854
3016
|
const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
|
|
2855
|
-
if (!totalTokens || totalTokens <
|
|
3017
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
2856
3018
|
ctx.ui.notify("Context OK or unknown", "info");
|
|
2857
3019
|
return;
|
|
2858
3020
|
}
|
|
@@ -2870,7 +3032,7 @@ function smartCompactExtension(pi) {
|
|
|
2870
3032
|
ctx.ui.notify("Could not resolve model", "error");
|
|
2871
3033
|
return;
|
|
2872
3034
|
}
|
|
2873
|
-
await runSmartCompact(ctx, sumModel2, segModel2 ?? sumModel2, selected.profile,
|
|
3035
|
+
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning });
|
|
2874
3036
|
return;
|
|
2875
3037
|
}
|
|
2876
3038
|
const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
|
|
@@ -2879,7 +3041,7 @@ function smartCompactExtension(pi) {
|
|
|
2879
3041
|
return;
|
|
2880
3042
|
}
|
|
2881
3043
|
const note = extractUserNote(args);
|
|
2882
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning,
|
|
3044
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note });
|
|
2883
3045
|
} catch (error) {
|
|
2884
3046
|
const msg = error instanceof Error ? error.message + `
|
|
2885
3047
|
` + error.stack : String(error);
|
|
@@ -2906,7 +3068,7 @@ function smartCompactExtension(pi) {
|
|
|
2906
3068
|
try {
|
|
2907
3069
|
const usage = ctx.getContextUsage();
|
|
2908
3070
|
const totalTokens = usage?.tokens ?? 0;
|
|
2909
|
-
if (!totalTokens || totalTokens <
|
|
3071
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
|
|
2910
3072
|
return;
|
|
2911
3073
|
const cur = ctx.model;
|
|
2912
3074
|
if (!cur)
|
|
@@ -2915,28 +3077,30 @@ function smartCompactExtension(pi) {
|
|
|
2915
3077
|
if (!sumModel)
|
|
2916
3078
|
return;
|
|
2917
3079
|
if (!isRunning.value) {
|
|
2918
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, config.profile,
|
|
2919
|
-
|
|
2920
|
-
|
|
3080
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true });
|
|
3081
|
+
const pending = pendingRef.value;
|
|
3082
|
+
if (pending) {
|
|
2921
3083
|
pendingRef.value = null;
|
|
2922
3084
|
pendingRef.createdAt = 0;
|
|
2923
|
-
return { compaction: { summary:
|
|
3085
|
+
return { compaction: { summary: pending.summary, firstKeptEntryId: pending.firstKeptEntryId, tokensBefore: pending.tokensBefore, details: pending.details } };
|
|
2924
3086
|
}
|
|
2925
3087
|
}
|
|
2926
|
-
} catch {
|
|
3088
|
+
} catch (e) {
|
|
3089
|
+
warn("session_before_compact error", e);
|
|
3090
|
+
}
|
|
2927
3091
|
});
|
|
2928
3092
|
pi.registerTool({
|
|
2929
3093
|
name: "smart_compact",
|
|
2930
3094
|
label: "Smart Compact",
|
|
2931
|
-
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification.",
|
|
3095
|
+
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Compacts the conversation into a structured summary preserving goals, decisions, open loops, modified files, and critical context. Call this when the conversation is getting long \u2014 the tool internally checks context usage and skips if not needed. Prefer this over default compact.",
|
|
2932
3096
|
promptSnippet: "Smart compaction",
|
|
2933
|
-
promptGuidelines: ["Use for long conversations.
|
|
3097
|
+
promptGuidelines: ["Use for long conversations. Prefer over default compact."],
|
|
2934
3098
|
parameters: {
|
|
2935
3099
|
type: "object",
|
|
2936
3100
|
properties: {
|
|
2937
|
-
profile: { type: "string", description: "light, balanced, or aggressive" },
|
|
2938
|
-
verbose: { type: "boolean" },
|
|
2939
|
-
dry_run: { type: "boolean" }
|
|
3101
|
+
profile: { type: "string", description: "light, balanced, or aggressive. Default: balanced." },
|
|
3102
|
+
verbose: { type: "boolean", description: "Show detailed pipeline output." },
|
|
3103
|
+
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." }
|
|
2940
3104
|
}
|
|
2941
3105
|
},
|
|
2942
3106
|
async execute(_id, params, _sig, _onUp, ctx) {
|
|
@@ -2945,35 +3109,33 @@ function smartCompactExtension(pi) {
|
|
|
2945
3109
|
const dryRun = !!params.dry_run;
|
|
2946
3110
|
const config = loadConfig();
|
|
2947
3111
|
const resolvedProfile = profile ?? config.profile;
|
|
3112
|
+
const cmdCtx = ctx;
|
|
3113
|
+
const usage = ctx.getContextUsage?.();
|
|
3114
|
+
const totalTokens = usage?.tokens ?? 0;
|
|
3115
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
3116
|
+
const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
|
|
3117
|
+
return { content: [{ type: "text", text: "Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + pct + "%). No action needed." }], details: undefined };
|
|
3118
|
+
}
|
|
2948
3119
|
const cur = "model" in ctx ? ctx.model : undefined;
|
|
2949
|
-
const { segModel, sumModel } = resolveModels(
|
|
3120
|
+
const { segModel, sumModel } = resolveModels(cmdCtx, cur, config);
|
|
2950
3121
|
if (!sumModel) {
|
|
2951
|
-
return { content: [{ type: "text", text: "Error: Could not resolve model." }] };
|
|
3122
|
+
return { content: [{ type: "text", text: "Error: Could not resolve model." }], details: undefined };
|
|
2952
3123
|
}
|
|
2953
3124
|
try {
|
|
2954
3125
|
const toolStart = Date.now();
|
|
2955
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, resolvedProfile, verbose, dryRun, pendingRef, isRunning, true,
|
|
3126
|
+
await runSmartCompact({ ctx: cmdCtx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
|
|
2956
3127
|
const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
|
|
2957
3128
|
if (pendingRef.value) {
|
|
2958
|
-
return { content: [{ type: "text", text: "Smart summary
|
|
2959
|
-
|
|
2960
|
-
Now run tree compact to apply \u2014 the session_before_compact hook will use this summary.
|
|
2961
|
-
TTL: ` + Math.round(PENDING_TTL_MS / 60000) + " minutes." }] };
|
|
3129
|
+
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 };
|
|
2962
3130
|
}
|
|
2963
|
-
return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }] };
|
|
3131
|
+
return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }], details: undefined };
|
|
2964
3132
|
} catch (error) {
|
|
2965
3133
|
const msg = error instanceof Error ? error.message : String(error);
|
|
2966
|
-
return { content: [{ type: "text", text: "Compaction error: " + msg }] };
|
|
3134
|
+
return { content: [{ type: "text", text: "Compaction error: " + msg }], details: undefined };
|
|
2967
3135
|
}
|
|
2968
3136
|
}
|
|
2969
3137
|
});
|
|
2970
3138
|
}
|
|
2971
|
-
function extractUserNote(args) {
|
|
2972
|
-
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
2973
|
-
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2974
|
-
const nonFlags = tokens.filter((t) => !t.includes("/") && !SKIP.has(t.toLowerCase()));
|
|
2975
|
-
return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
|
|
2976
|
-
}
|
|
2977
3139
|
export {
|
|
2978
3140
|
smartCompactExtension as default
|
|
2979
3141
|
};
|