pi-smart-compact 7.7.0 → 7.9.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 +32 -0
- package/README.md +407 -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 +264 -112
- 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.0";
|
|
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];
|
|
@@ -685,8 +802,8 @@ function catalogErrors(msgs) {
|
|
|
685
802
|
}
|
|
686
803
|
return errors;
|
|
687
804
|
}
|
|
688
|
-
function extractDecisions(msgs) {
|
|
689
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
805
|
+
function extractDecisions(msgs, _tcIdx) {
|
|
806
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
690
807
|
const decisions = [];
|
|
691
808
|
for (const [id, tc] of tcIdx) {
|
|
692
809
|
if (tc.name !== "ask_user")
|
|
@@ -737,10 +854,10 @@ function mineConstraints(msgs) {
|
|
|
737
854
|
}
|
|
738
855
|
return constraints;
|
|
739
856
|
}
|
|
740
|
-
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20) {
|
|
857
|
+
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
741
858
|
const topics = [];
|
|
742
859
|
let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
|
|
743
|
-
const tcIdx = buildToolCallIndex(msgs);
|
|
860
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
744
861
|
for (let i = 0;i < msgs.length; i++) {
|
|
745
862
|
const m = msgs[i];
|
|
746
863
|
const txt = extractText(m.content);
|
|
@@ -896,11 +1013,12 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
896
1013
|
return loops;
|
|
897
1014
|
}
|
|
898
1015
|
function extractStructured(msgs, pc) {
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
const
|
|
1016
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
1017
|
+
const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
|
|
1018
|
+
const errors = catalogErrors(msgs, tcIdx);
|
|
1019
|
+
const decisions = extractDecisions(msgs, tcIdx);
|
|
902
1020
|
const constraints = mineConstraints(msgs);
|
|
903
|
-
const topics = segmentTopicsHeuristic(msgs, pc);
|
|
1021
|
+
const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
|
|
904
1022
|
const timeline = buildTimeline(msgs, errors);
|
|
905
1023
|
const mainGoal = extractMainGoal(msgs);
|
|
906
1024
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
@@ -933,7 +1051,9 @@ function saveCompactionState(projectId, state) {
|
|
|
933
1051
|
if (!fs3.existsSync(STATE_DIR))
|
|
934
1052
|
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
935
1053
|
fs3.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
|
|
936
|
-
} catch {
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
warn("saveCompactionState failed", e);
|
|
1056
|
+
}
|
|
937
1057
|
}
|
|
938
1058
|
function loadCompactionState(projectId) {
|
|
939
1059
|
try {
|
|
@@ -941,9 +1061,22 @@ function loadCompactionState(projectId) {
|
|
|
941
1061
|
if (!fs3.existsSync(fp))
|
|
942
1062
|
return null;
|
|
943
1063
|
const data = JSON.parse(fs3.readFileSync(fp, "utf8"));
|
|
944
|
-
if (data.compactionVersion
|
|
1064
|
+
if (data.compactionVersion) {
|
|
1065
|
+
let updatedAt = data.updatedAt;
|
|
1066
|
+
if (!updatedAt) {
|
|
1067
|
+
try {
|
|
1068
|
+
updatedAt = fs3.statSync(fp).mtimeMs;
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
debug("statSync failed for state file", e);
|
|
1071
|
+
updatedAt = 0;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (Date.now() - updatedAt > 7 * 24 * 60 * 60 * 1000)
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
945
1077
|
return data;
|
|
946
|
-
} catch {
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
warn("loadCompactionState failed", e);
|
|
947
1080
|
return null;
|
|
948
1081
|
}
|
|
949
1082
|
}
|
|
@@ -991,7 +1124,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
991
1124
|
nextActions,
|
|
992
1125
|
criticalContext,
|
|
993
1126
|
sessionType: report?.sessionType ?? "implementation",
|
|
994
|
-
compactionVersion: VERSION
|
|
1127
|
+
compactionVersion: VERSION,
|
|
1128
|
+
updatedAt: Date.now()
|
|
995
1129
|
};
|
|
996
1130
|
}
|
|
997
1131
|
function injectOpenLoopsSection(summary, openLoops) {
|
|
@@ -1220,6 +1354,7 @@ function pruneRedundant(msgs) {
|
|
|
1220
1354
|
// src/utils/fingerprint.ts
|
|
1221
1355
|
import fs4 from "fs";
|
|
1222
1356
|
import path5 from "path";
|
|
1357
|
+
import crypto3 from "crypto";
|
|
1223
1358
|
var FINGERPRINT_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
|
|
1224
1359
|
var LANG_MAP = {
|
|
1225
1360
|
".ts": "typescript",
|
|
@@ -1269,11 +1404,8 @@ function deriveProjectId(extraction) {
|
|
|
1269
1404
|
roots.set(root, (roots.get(root) ?? 0) + 1);
|
|
1270
1405
|
}
|
|
1271
1406
|
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);
|
|
1407
|
+
const hash = crypto3.createHash("sha256").update(topRoot).digest("hex").slice(0, 12);
|
|
1408
|
+
return "proj-" + hash;
|
|
1277
1409
|
}
|
|
1278
1410
|
function detectLanguage(extraction) {
|
|
1279
1411
|
const extCounts = new Map;
|
|
@@ -1321,7 +1453,8 @@ function loadProjectFingerprint(projectId) {
|
|
|
1321
1453
|
if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
|
|
1322
1454
|
return null;
|
|
1323
1455
|
return data;
|
|
1324
|
-
} catch {
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
warn("loadProjectFingerprint failed", e);
|
|
1325
1458
|
return null;
|
|
1326
1459
|
}
|
|
1327
1460
|
}
|
|
@@ -1345,7 +1478,9 @@ function saveProjectFingerprint(projectId, extraction) {
|
|
|
1345
1478
|
updatedAt: Date.now()
|
|
1346
1479
|
};
|
|
1347
1480
|
fs4.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
|
|
1348
|
-
} catch {
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
warn("saveProjectFingerprint failed", e);
|
|
1483
|
+
}
|
|
1349
1484
|
}
|
|
1350
1485
|
function buildProjectContext(fingerprint) {
|
|
1351
1486
|
if (!fingerprint)
|
|
@@ -1459,7 +1594,9 @@ function logDamageReport(sessionId, report, details) {
|
|
|
1459
1594
|
};
|
|
1460
1595
|
fs5.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
1461
1596
|
`);
|
|
1462
|
-
} catch {
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
warn("logDamageReport failed", e);
|
|
1599
|
+
}
|
|
1463
1600
|
}
|
|
1464
1601
|
|
|
1465
1602
|
// src/phases/explore.ts
|
|
@@ -1525,7 +1662,13 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
1525
1662
|
}
|
|
1526
1663
|
case "search_conversation": {
|
|
1527
1664
|
const q = (args.query ?? "").toLowerCase();
|
|
1528
|
-
return JSON.stringify(llmMessages.filter((m) =>
|
|
1665
|
+
return JSON.stringify(llmMessages.filter((m) => {
|
|
1666
|
+
const text = extractText(m?.content).toLowerCase();
|
|
1667
|
+
if (text.includes(q))
|
|
1668
|
+
return true;
|
|
1669
|
+
const tcs = filterToolCalls(m?.content);
|
|
1670
|
+
return tcs.some((tc) => JSON.stringify(tc.arguments).toLowerCase().includes(q));
|
|
1671
|
+
}).slice(0, 10).map((m) => ({
|
|
1529
1672
|
idx: llmMessages.indexOf(m),
|
|
1530
1673
|
role: m?.role,
|
|
1531
1674
|
preview: extractText(m?.content).slice(0, 150)
|
|
@@ -1589,11 +1732,15 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
1589
1732
|
let rawJson = json.slice(s, e + 1);
|
|
1590
1733
|
try {
|
|
1591
1734
|
return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
|
|
1592
|
-
} catch {
|
|
1735
|
+
} catch (e2) {
|
|
1736
|
+
debug("JSON parse attempt 1 failed", e2);
|
|
1737
|
+
}
|
|
1593
1738
|
const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"').replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1594
1739
|
try {
|
|
1595
1740
|
return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
|
|
1596
|
-
} catch {
|
|
1741
|
+
} catch (e2) {
|
|
1742
|
+
debug("JSON parse attempt 2 (cleaned) failed", e2);
|
|
1743
|
+
}
|
|
1597
1744
|
const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
|
|
1598
1745
|
if (boundaryMatch) {
|
|
1599
1746
|
try {
|
|
@@ -1604,7 +1751,9 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
1604
1751
|
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
1605
1752
|
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
1606
1753
|
})) };
|
|
1607
|
-
} catch {
|
|
1754
|
+
} catch (e2) {
|
|
1755
|
+
debug("Boundary JSON parse failed", e2);
|
|
1756
|
+
}
|
|
1608
1757
|
}
|
|
1609
1758
|
return fallbackExplorationReport(llmMessages);
|
|
1610
1759
|
}
|
|
@@ -1641,7 +1790,7 @@ function fallbackExplorationReport(llmMessages) {
|
|
|
1641
1790
|
keyDecisions: []
|
|
1642
1791
|
};
|
|
1643
1792
|
}
|
|
1644
|
-
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds =
|
|
1793
|
+
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify) {
|
|
1645
1794
|
const extractionContext = [
|
|
1646
1795
|
"## Deterministic Extraction (verified facts)",
|
|
1647
1796
|
"Message count: " + extraction.messageCount,
|
|
@@ -1679,13 +1828,13 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1679
1828
|
if (retried.boundaries.length)
|
|
1680
1829
|
return { report: retried, rounds: 1, toolSupported: false };
|
|
1681
1830
|
}
|
|
1682
|
-
return { report: report2, rounds:
|
|
1831
|
+
return { report: report2, rounds: 1, toolSupported: false };
|
|
1683
1832
|
}
|
|
1684
1833
|
const probeResp = await trackedComplete("explore", model, {
|
|
1685
1834
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1686
1835
|
messages: [{ role: "user", content: [{ type: "text", text: userContent }] }],
|
|
1687
1836
|
tools: EXPLORATION_TOOLS
|
|
1688
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1837
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
|
|
1689
1838
|
const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
|
|
1690
1839
|
if (toolCalls.length > 0) {
|
|
1691
1840
|
supportsTools = true;
|
|
@@ -1709,9 +1858,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1709
1858
|
` + EXPLORER_SYSTEM_PROMPT,
|
|
1710
1859
|
messages,
|
|
1711
1860
|
tools: EXPLORATION_TOOLS
|
|
1712
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1861
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }, model.provider));
|
|
1713
1862
|
} catch (err) {
|
|
1714
|
-
|
|
1863
|
+
warn("Explore loop error", err);
|
|
1715
1864
|
break;
|
|
1716
1865
|
}
|
|
1717
1866
|
const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
|
|
@@ -1749,8 +1898,8 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1749
1898
|
}
|
|
1750
1899
|
return { report: report2, rounds: 1, toolSupported: true };
|
|
1751
1900
|
}
|
|
1752
|
-
} catch {
|
|
1753
|
-
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
warn("Tool calling probe failed for " + cacheKey, e);
|
|
1754
1903
|
_toolSupportCache.set(cacheKey, { result: false, timestamp: Date.now() });
|
|
1755
1904
|
if (notify)
|
|
1756
1905
|
notify("Tool calling not supported, using direct exploration", "warning");
|
|
@@ -1761,7 +1910,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
1761
1910
|
if (retried.boundaries.length)
|
|
1762
1911
|
return { report: retried, rounds: 1, toolSupported: false };
|
|
1763
1912
|
}
|
|
1764
|
-
return { report, rounds:
|
|
1913
|
+
return { report, rounds: 1, toolSupported: supportsTools };
|
|
1765
1914
|
}
|
|
1766
1915
|
async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal) {
|
|
1767
1916
|
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
|
|
@@ -1781,10 +1930,11 @@ User steering: ` + userNote : "");
|
|
|
1781
1930
|
const resp = await trackedComplete("explore-retry", model, {
|
|
1782
1931
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1783
1932
|
messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }] }]
|
|
1784
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1933
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1785
1934
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
1786
1935
|
return parseExplorationReport(text, llmMessages);
|
|
1787
|
-
} catch {
|
|
1936
|
+
} catch (e) {
|
|
1937
|
+
debug("explorationRetry failed", e);
|
|
1788
1938
|
return fallbackExplorationReport(llmMessages);
|
|
1789
1939
|
}
|
|
1790
1940
|
}
|
|
@@ -1818,16 +1968,20 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
1818
1968
|
const resp = await trackedComplete("explore-direct", model, {
|
|
1819
1969
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1820
1970
|
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }]
|
|
1821
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1971
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1822
1972
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1823
1973
|
`).trim();
|
|
1824
1974
|
return parseExplorationReport(text, llmMessages);
|
|
1825
|
-
} catch {
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
debug("directExploration failed", e);
|
|
1826
1977
|
return fallbackExplorationReport(llmMessages);
|
|
1827
1978
|
}
|
|
1828
1979
|
}
|
|
1829
1980
|
|
|
1830
1981
|
// src/phases/synthesize.ts
|
|
1982
|
+
function estimateChunkTokens(msgs) {
|
|
1983
|
+
return estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
|
|
1984
|
+
}
|
|
1831
1985
|
function chunkLlmMessages(msgs, boundaries, pc) {
|
|
1832
1986
|
if (!msgs.length)
|
|
1833
1987
|
return [];
|
|
@@ -1835,7 +1989,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1835
1989
|
return [{
|
|
1836
1990
|
startIndex: 0,
|
|
1837
1991
|
endIndex: msgs.length - 1,
|
|
1838
|
-
tokenEstimate:
|
|
1992
|
+
tokenEstimate: estimateChunkTokens(msgs),
|
|
1839
1993
|
topic: "Full conversation",
|
|
1840
1994
|
priority: "normal",
|
|
1841
1995
|
messages: msgs
|
|
@@ -1851,7 +2005,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1851
2005
|
chunks.push({
|
|
1852
2006
|
startIndex: start,
|
|
1853
2007
|
endIndex: end - 1,
|
|
1854
|
-
tokenEstimate:
|
|
2008
|
+
tokenEstimate: estimateChunkTokens(slice),
|
|
1855
2009
|
topic: bp.topic || "Segment " + (chunks.length + 1),
|
|
1856
2010
|
priority: bp.priority,
|
|
1857
2011
|
messages: slice
|
|
@@ -1865,7 +2019,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
1865
2019
|
chunks.push({
|
|
1866
2020
|
startIndex: start,
|
|
1867
2021
|
endIndex: msgs.length - 1,
|
|
1868
|
-
tokenEstimate:
|
|
2022
|
+
tokenEstimate: estimateChunkTokens(slice),
|
|
1869
2023
|
topic: lastTopic,
|
|
1870
2024
|
priority: "normal",
|
|
1871
2025
|
messages: slice
|
|
@@ -1900,7 +2054,7 @@ Session-specific instructions:
|
|
|
1900
2054
|
{ role: "user", content: [{ type: "text", text: adaptedPrefix }] },
|
|
1901
2055
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1902
2056
|
]
|
|
1903
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens:
|
|
2057
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal }, model.provider));
|
|
1904
2058
|
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1905
2059
|
`).trim();
|
|
1906
2060
|
if (!summary.startsWith("##"))
|
|
@@ -1931,7 +2085,7 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
|
1931
2085
|
{ role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }] },
|
|
1932
2086
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1933
2087
|
]
|
|
1934
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
2088
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1935
2089
|
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1936
2090
|
`);
|
|
1937
2091
|
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
@@ -1972,7 +2126,7 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
|
|
|
1972
2126
|
{ role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }] },
|
|
1973
2127
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1974
2128
|
]
|
|
1975
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget,
|
|
2129
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal }, model.provider));
|
|
1976
2130
|
return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1977
2131
|
`).trim();
|
|
1978
2132
|
}
|
|
@@ -2120,10 +2274,14 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2120
2274
|
const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
|
|
2121
2275
|
const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
|
|
2122
2276
|
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"));
|
|
2277
|
+
const findSectionInsert = (header) => {
|
|
2278
|
+
const re = new RegExp(header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\n", "i");
|
|
2279
|
+
const m = patched.match(re);
|
|
2280
|
+
return m?.index != null ? m.index + m[0].length : null;
|
|
2281
|
+
};
|
|
2123
2282
|
if (fileGaps.length > 0) {
|
|
2124
|
-
const
|
|
2125
|
-
if (
|
|
2126
|
-
const insertPos = filesSection.index + filesSection[0].length;
|
|
2283
|
+
const insertPos = findSectionInsert("## Files Modified");
|
|
2284
|
+
if (insertPos != null) {
|
|
2127
2285
|
const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
|
|
2128
2286
|
`) + `
|
|
2129
2287
|
`;
|
|
@@ -2131,9 +2289,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2131
2289
|
}
|
|
2132
2290
|
}
|
|
2133
2291
|
if (errorGaps.length > 0) {
|
|
2134
|
-
const
|
|
2135
|
-
if (
|
|
2136
|
-
const insertPos = ctxSection.index + ctxSection[0].length;
|
|
2292
|
+
const insertPos = findSectionInsert("## Critical Context");
|
|
2293
|
+
if (insertPos != null) {
|
|
2137
2294
|
const entries = errorGaps.map((g) => "- " + g).join(`
|
|
2138
2295
|
`) + `
|
|
2139
2296
|
`;
|
|
@@ -2141,9 +2298,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2141
2298
|
}
|
|
2142
2299
|
}
|
|
2143
2300
|
if (constraintGaps.length > 0) {
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
const insertPos = constrSection.index + constrSection[0].length;
|
|
2301
|
+
const insertPos = findSectionInsert("## Constraints & Preferences");
|
|
2302
|
+
if (insertPos != null) {
|
|
2147
2303
|
const entries = constraintGaps.map((g) => "- " + g).join(`
|
|
2148
2304
|
`) + `
|
|
2149
2305
|
`;
|
|
@@ -2151,9 +2307,8 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
2151
2307
|
}
|
|
2152
2308
|
}
|
|
2153
2309
|
if (decisionGaps.length > 0) {
|
|
2154
|
-
const
|
|
2155
|
-
if (
|
|
2156
|
-
const insertPos = decSection.index + decSection[0].length;
|
|
2310
|
+
const insertPos = findSectionInsert("## Key Decisions");
|
|
2311
|
+
if (insertPos != null) {
|
|
2157
2312
|
const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
|
|
2158
2313
|
`) + `
|
|
2159
2314
|
`;
|
|
@@ -2187,7 +2342,8 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
|
|
|
2187
2342
|
const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2188
2343
|
`).trim();
|
|
2189
2344
|
return patched.startsWith("##") ? patched : summary;
|
|
2190
|
-
} catch {
|
|
2345
|
+
} catch (e) {
|
|
2346
|
+
debug("patchSummary LLM failed", e);
|
|
2191
2347
|
return summary;
|
|
2192
2348
|
}
|
|
2193
2349
|
}
|
|
@@ -2211,7 +2367,6 @@ function renderTokenBar(theme, before, after, label, barLen = 30) {
|
|
|
2211
2367
|
const savedColor = savedPct >= 50 ? "success" : savedPct >= 25 ? "warning" : "error";
|
|
2212
2368
|
return theme.fg("text", " " + label + ": ") + theme.fg(savedColor, bar) + theme.fg("text", " " + (after ?? 0).toLocaleString() + "t") + theme.fg(savedColor, " (saved " + savedPct + "%)");
|
|
2213
2369
|
}
|
|
2214
|
-
var _toolSupportCache2 = new Map;
|
|
2215
2370
|
async function selectModel(ctx, opts) {
|
|
2216
2371
|
const available = ctx.modelRegistry.getAvailable();
|
|
2217
2372
|
const options = available.map((m) => ({
|
|
@@ -2435,7 +2590,8 @@ async function showCompactUI(ctx, opts) {
|
|
|
2435
2590
|
}
|
|
2436
2591
|
|
|
2437
2592
|
// src/core.ts
|
|
2438
|
-
async function runSmartCompact(
|
|
2593
|
+
async function runSmartCompact(opts) {
|
|
2594
|
+
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
|
|
2439
2595
|
if (isRunning.value)
|
|
2440
2596
|
return;
|
|
2441
2597
|
isRunning.value = true;
|
|
@@ -2461,7 +2617,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2461
2617
|
}
|
|
2462
2618
|
const usage = ctx.getContextUsage();
|
|
2463
2619
|
const totalTokens = usage?.tokens ?? 0;
|
|
2464
|
-
if (!totalTokens || totalTokens <
|
|
2620
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
2465
2621
|
isRunning.value = false;
|
|
2466
2622
|
if (!autoTriggered)
|
|
2467
2623
|
ctx.ui.notify("Context OK or unknown", "info");
|
|
@@ -2558,7 +2714,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2558
2714
|
if (!autoTriggered)
|
|
2559
2715
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
|
|
2560
2716
|
try {
|
|
2561
|
-
const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal,
|
|
2717
|
+
const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, MAX_EXPLORATION_ROUNDS, notify);
|
|
2562
2718
|
explorationReport = expResult.report;
|
|
2563
2719
|
explorationRounds = expResult.rounds;
|
|
2564
2720
|
notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
@@ -2668,7 +2824,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2668
2824
|
else
|
|
2669
2825
|
throw new Error("bad");
|
|
2670
2826
|
} catch (err) {
|
|
2671
|
-
|
|
2827
|
+
warn("Assembly failed", err);
|
|
2672
2828
|
finalSummary = assembleFallback(summaries, extraction);
|
|
2673
2829
|
assemblyCalls = 0;
|
|
2674
2830
|
}
|
|
@@ -2689,7 +2845,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2689
2845
|
finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
|
|
2690
2846
|
llmCalls++;
|
|
2691
2847
|
} catch (err) {
|
|
2692
|
-
|
|
2848
|
+
warn("LLM patch failed", err);
|
|
2693
2849
|
}
|
|
2694
2850
|
}
|
|
2695
2851
|
} else {
|
|
@@ -2752,7 +2908,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2752
2908
|
saveCompactionState(projectId, compactionState);
|
|
2753
2909
|
appendMetricsLog(sessionId);
|
|
2754
2910
|
try {
|
|
2755
|
-
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat()
|
|
2911
|
+
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
|
|
2756
2912
|
if (postCompactMsgs.length > 2) {
|
|
2757
2913
|
const lastCompaction = branch.filter((e) => e.type === "compaction").slice(-1)[0];
|
|
2758
2914
|
if (lastCompaction?.details) {
|
|
@@ -2765,7 +2921,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2765
2921
|
}
|
|
2766
2922
|
}
|
|
2767
2923
|
} catch (err) {
|
|
2768
|
-
|
|
2924
|
+
warn("Damage detection error", err);
|
|
2769
2925
|
}
|
|
2770
2926
|
const ms = getMetricsSummary();
|
|
2771
2927
|
if (ms.totalCalls > 0) {
|
|
@@ -2776,7 +2932,7 @@ async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dr
|
|
|
2776
2932
|
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
2777
2933
|
await Promise.race([showResultScreen(ctx, details, extraction), timeout]);
|
|
2778
2934
|
} catch (err) {
|
|
2779
|
-
|
|
2935
|
+
warn("Result screen error", err);
|
|
2780
2936
|
notify("Result screen skipped", "info");
|
|
2781
2937
|
}
|
|
2782
2938
|
}
|
|
@@ -2852,7 +3008,7 @@ function smartCompactExtension(pi) {
|
|
|
2852
3008
|
const usage = ctx.getContextUsage();
|
|
2853
3009
|
const totalTokens = usage?.tokens ?? 0;
|
|
2854
3010
|
const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
|
|
2855
|
-
if (!totalTokens || totalTokens <
|
|
3011
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
2856
3012
|
ctx.ui.notify("Context OK or unknown", "info");
|
|
2857
3013
|
return;
|
|
2858
3014
|
}
|
|
@@ -2870,7 +3026,7 @@ function smartCompactExtension(pi) {
|
|
|
2870
3026
|
ctx.ui.notify("Could not resolve model", "error");
|
|
2871
3027
|
return;
|
|
2872
3028
|
}
|
|
2873
|
-
await runSmartCompact(ctx, sumModel2, segModel2 ?? sumModel2, selected.profile,
|
|
3029
|
+
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning });
|
|
2874
3030
|
return;
|
|
2875
3031
|
}
|
|
2876
3032
|
const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
|
|
@@ -2879,7 +3035,7 @@ function smartCompactExtension(pi) {
|
|
|
2879
3035
|
return;
|
|
2880
3036
|
}
|
|
2881
3037
|
const note = extractUserNote(args);
|
|
2882
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning,
|
|
3038
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note });
|
|
2883
3039
|
} catch (error) {
|
|
2884
3040
|
const msg = error instanceof Error ? error.message + `
|
|
2885
3041
|
` + error.stack : String(error);
|
|
@@ -2906,7 +3062,7 @@ function smartCompactExtension(pi) {
|
|
|
2906
3062
|
try {
|
|
2907
3063
|
const usage = ctx.getContextUsage();
|
|
2908
3064
|
const totalTokens = usage?.tokens ?? 0;
|
|
2909
|
-
if (!totalTokens || totalTokens <
|
|
3065
|
+
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
|
|
2910
3066
|
return;
|
|
2911
3067
|
const cur = ctx.model;
|
|
2912
3068
|
if (!cur)
|
|
@@ -2915,7 +3071,7 @@ function smartCompactExtension(pi) {
|
|
|
2915
3071
|
if (!sumModel)
|
|
2916
3072
|
return;
|
|
2917
3073
|
if (!isRunning.value) {
|
|
2918
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, config.profile,
|
|
3074
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true });
|
|
2919
3075
|
if (pendingRef.value) {
|
|
2920
3076
|
const c = pendingRef.value;
|
|
2921
3077
|
pendingRef.value = null;
|
|
@@ -2923,7 +3079,9 @@ function smartCompactExtension(pi) {
|
|
|
2923
3079
|
return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
|
|
2924
3080
|
}
|
|
2925
3081
|
}
|
|
2926
|
-
} catch {
|
|
3082
|
+
} catch (e) {
|
|
3083
|
+
warn("session_before_compact error", e);
|
|
3084
|
+
}
|
|
2927
3085
|
});
|
|
2928
3086
|
pi.registerTool({
|
|
2929
3087
|
name: "smart_compact",
|
|
@@ -2952,7 +3110,7 @@ function smartCompactExtension(pi) {
|
|
|
2952
3110
|
}
|
|
2953
3111
|
try {
|
|
2954
3112
|
const toolStart = Date.now();
|
|
2955
|
-
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, resolvedProfile, verbose, dryRun, pendingRef, isRunning, true,
|
|
3113
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
|
|
2956
3114
|
const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
|
|
2957
3115
|
if (pendingRef.value) {
|
|
2958
3116
|
return { content: [{ type: "text", text: "Smart summary generated (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? "?") + " -> " + (pendingRef.value.summary?.length ?? 0) + " chars (" + toolSecs + `s).
|
|
@@ -2968,12 +3126,6 @@ TTL: ` + Math.round(PENDING_TTL_MS / 60000) + " minutes." }] };
|
|
|
2968
3126
|
}
|
|
2969
3127
|
});
|
|
2970
3128
|
}
|
|
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
3129
|
export {
|
|
2978
3130
|
smartCompactExtension as default
|
|
2979
3131
|
};
|