pi-smart-compact 7.15.1 → 7.18.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 +75 -0
- package/README.md +166 -285
- package/SECURITY.md +14 -5
- package/SUPPORT.md +18 -5
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/persist.d.ts.map +1 -1
- package/dist/app/steps/recover.d.ts.map +1 -1
- package/dist/app/steps/state.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/constants.d.ts +60 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/domain/keywords.d.ts +20 -0
- package/dist/domain/keywords.d.ts.map +1 -0
- package/dist/domain/tool-semantics.d.ts +45 -0
- package/dist/domain/tool-semantics.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1624 -1226
- package/dist/infra/ai-messages.d.ts +40 -0
- package/dist/infra/ai-messages.d.ts.map +1 -0
- package/dist/infra/paths.d.ts +2 -0
- package/dist/infra/paths.d.ts.map +1 -1
- package/dist/infra/services.d.ts +1 -0
- package/dist/infra/services.d.ts.map +1 -1
- package/dist/phases/explore.d.ts +1 -1
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts +7 -0
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlays.d.ts +7 -0
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/damage.d.ts +14 -0
- package/dist/utils/damage.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/fingerprint.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +33 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/pruning.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/dist/utils/state.d.ts +7 -0
- package/dist/utils/state.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts.map +1 -1
- package/docs/RELEASE.md +1 -1
- package/docs/assets/banner.svg +71 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// src/constants.ts
|
|
19
|
-
var VERSION = "7.
|
|
19
|
+
var VERSION = "7.18.0", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
|
|
20
20
|
{PREV_CONTEXT}
|
|
21
21
|
|
|
22
22
|
{EXTRACTION_CONTEXT}
|
|
@@ -39,7 +39,7 @@ IMMUTABLE CONTEXT (verified deterministic data):
|
|
|
39
39
|
{EXPLORATION_CONTEXT}
|
|
40
40
|
{PREV_CONTEXT}
|
|
41
41
|
|
|
42
|
-
<summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, MAX_TOOL_OUTPUT_CHARS = 800, METRICS_BUFFER_MAX = 200, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
|
|
42
|
+
<summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, FIVE_MINUTES_MS, ONE_HOUR_MS, SEVEN_DAYS_MS, THIRTY_DAYS_MS, EXTRACTION_CACHE_PREFIX = "compact-extraction-", ID_PREFIX, TUNING, TRUNC, MAX_TOOL_OUTPUT_CHARS = 800, LIKELY_ERROR_RE, ERROR_SCAN_MAX_LEN = 2000, ERROR_RETRY_WINDOW = 6, ERROR_RESOLVE_WINDOW = 10, METRICS_BUFFER_MAX = 200, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
|
|
43
43
|
var init_constants = __esm(() => {
|
|
44
44
|
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.";
|
|
45
45
|
PROFILES = {
|
|
@@ -77,10 +77,11 @@ var init_constants = __esm(() => {
|
|
|
77
77
|
autoTriggerTimeoutMs: 120000,
|
|
78
78
|
backupEnabled: true,
|
|
79
79
|
backupDir: "",
|
|
80
|
-
minContextPercent: 60
|
|
80
|
+
minContextPercent: 60,
|
|
81
|
+
pinPaths: []
|
|
81
82
|
};
|
|
82
83
|
NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
|
|
83
|
-
SHIFT_RE =
|
|
84
|
+
SHIFT_RE = /\u015Fimdi|simdi|peki|bi de|bide|ge\u00E7elim|gecelim|bakal\u0131m|bakalim|yapal\u0131m|yapalim|ba\u015Fka|baska|sonra|tamam \u015Fimdi|tamam simdi|now let|also|next|let's|moving on|switch to/iu;
|
|
84
85
|
CHOICE_RE = /use\s+\S+\s+(?:instead|not|rather)|don't\s+use|avoid\s+|switch\s+to\s+|go\s+with\s+|prefer\s+/i;
|
|
85
86
|
SINGLE_PASS_PREFIX = `Summarize this coding agent conversation. Produce ONE structured summary.
|
|
86
87
|
` + `
|
|
@@ -177,6 +178,56 @@ Rules for Accuracy:
|
|
|
177
178
|
discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
|
|
178
179
|
};
|
|
179
180
|
BACKUP_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
181
|
+
FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
182
|
+
ONE_HOUR_MS = 60 * 60 * 1000;
|
|
183
|
+
SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
184
|
+
THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
185
|
+
ID_PREFIX = {
|
|
186
|
+
PROJECT: "proj-",
|
|
187
|
+
COMPACT_SESSION: "sc-",
|
|
188
|
+
MULTI_TOOL_USE_SYNTHETIC: "mtu_",
|
|
189
|
+
OPEN_LOOP: "loop-",
|
|
190
|
+
DECISION: "decision-",
|
|
191
|
+
ERROR: "error-"
|
|
192
|
+
};
|
|
193
|
+
TUNING = {
|
|
194
|
+
EMA_PREV: 0.7,
|
|
195
|
+
EMA_SAMPLE: 0.3,
|
|
196
|
+
CALIBRATION_CLAMP_MIN: 0.3,
|
|
197
|
+
CALIBRATION_CLAMP_MAX: 3,
|
|
198
|
+
CONFIDENCE_HIGH: 0.85,
|
|
199
|
+
CONFIDENCE_MEDIUM: 0.8,
|
|
200
|
+
CONFIDENCE_LOW: 0.6
|
|
201
|
+
};
|
|
202
|
+
TRUNC = {
|
|
203
|
+
MESSAGE: 300,
|
|
204
|
+
ERROR_DETAIL: 500,
|
|
205
|
+
DECISION_SUMMARY: 200,
|
|
206
|
+
USER_RESPONSE: 300,
|
|
207
|
+
CONSTRAINT_TEXT: 300,
|
|
208
|
+
OPEN_LOOP_SUMMARY: 120,
|
|
209
|
+
TIMELINE_EVENT: 150,
|
|
210
|
+
TIMELINE_ERROR: 100,
|
|
211
|
+
SNIPPET: 80,
|
|
212
|
+
PREVIEW: 150,
|
|
213
|
+
PREVIEW_MID: 200,
|
|
214
|
+
DETAIL: 300,
|
|
215
|
+
PREVIEW_LONG: 400,
|
|
216
|
+
PREVIEW_XL: 500,
|
|
217
|
+
CHUNK_FALLBACK: 180,
|
|
218
|
+
DECISION_DETAIL: 60,
|
|
219
|
+
TOPIC_LABEL: 100,
|
|
220
|
+
PROJ_ID_HASH: 12,
|
|
221
|
+
CONV_HASH: 8,
|
|
222
|
+
RESULT_GAPS: 5,
|
|
223
|
+
SESSION_ID_DISPLAY: 20,
|
|
224
|
+
ERROR_SNIPPET: 30,
|
|
225
|
+
TIMELINE_DISPLAY: 10,
|
|
226
|
+
EXPLORE_RESULTS: 15,
|
|
227
|
+
BACKUP_PREVIEW_LINES: 5,
|
|
228
|
+
FINGERPRINT_SEG: 2
|
|
229
|
+
};
|
|
230
|
+
LIKELY_ERROR_RE = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i;
|
|
180
231
|
EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
|
|
181
232
|
|
|
182
233
|
` + `Your job:
|
|
@@ -246,7 +297,7 @@ function damageReportsFile() {
|
|
|
246
297
|
return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
247
298
|
}
|
|
248
299
|
function extractionCacheFile(sessionId) {
|
|
249
|
-
return path.join(cacheDir(),
|
|
300
|
+
return path.join(cacheDir(), EXTRACTION_CACHE_PREFIX + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
|
|
250
301
|
}
|
|
251
302
|
function projectFingerprintFile(projectId) {
|
|
252
303
|
return path.join(projectFingerprintDir(), projectId + ".json");
|
|
@@ -254,10 +305,15 @@ function projectFingerprintFile(projectId) {
|
|
|
254
305
|
function compactionStateFile(projectId) {
|
|
255
306
|
return path.join(compactionStateDir(), projectId + ".json");
|
|
256
307
|
}
|
|
308
|
+
function remediationHintsFile(projectId) {
|
|
309
|
+
return path.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
|
|
310
|
+
}
|
|
257
311
|
function metricsDashboardFile() {
|
|
258
312
|
return path.join(cacheDir(), "smart-compact-report.html");
|
|
259
313
|
}
|
|
260
|
-
var init_paths = () => {
|
|
314
|
+
var init_paths = __esm(() => {
|
|
315
|
+
init_constants();
|
|
316
|
+
});
|
|
261
317
|
|
|
262
318
|
// src/infra/fs.ts
|
|
263
319
|
import fs from "fs";
|
|
@@ -385,8 +441,8 @@ class TokenCalibrationStore {
|
|
|
385
441
|
const key = calibrationKey(provider, model);
|
|
386
442
|
const prev = this.factors.get(key) ?? 1;
|
|
387
443
|
const sample = actual / estimated;
|
|
388
|
-
const clamped = Math.max(
|
|
389
|
-
this.factors.set(key, prev *
|
|
444
|
+
const clamped = Math.max(TUNING.CALIBRATION_CLAMP_MIN, Math.min(TUNING.CALIBRATION_CLAMP_MAX, sample));
|
|
445
|
+
this.factors.set(key, prev * TUNING.EMA_PREV + clamped * TUNING.EMA_SAMPLE);
|
|
390
446
|
}
|
|
391
447
|
}
|
|
392
448
|
function calibrationKey(provider, model) {
|
|
@@ -394,7 +450,21 @@ function calibrationKey(provider, model) {
|
|
|
394
450
|
}
|
|
395
451
|
function estimateTokens(text, provider, model, calibration = _fallbackCalibration) {
|
|
396
452
|
const baseRatio = provider ? getProviderCaps(provider).tokenRatioEstimate : CHARS_PER_TOKEN;
|
|
397
|
-
const
|
|
453
|
+
const startsJson = text.startsWith("[") || text.startsWith("{");
|
|
454
|
+
let jsonPenalty = 1;
|
|
455
|
+
if (startsJson) {
|
|
456
|
+
jsonPenalty = JSON_PENALTY;
|
|
457
|
+
} else if (text.length > 0) {
|
|
458
|
+
const sample = text.length > JSON_DENSITY_SCAN_CAP ? text.slice(0, JSON_DENSITY_SCAN_CAP) : text;
|
|
459
|
+
let structural = 0;
|
|
460
|
+
for (let i = 0;i < sample.length; i++) {
|
|
461
|
+
const c = sample.charCodeAt(i);
|
|
462
|
+
if (c === 34 || c === 91 || c === 93 || c === 123 || c === 125)
|
|
463
|
+
structural++;
|
|
464
|
+
}
|
|
465
|
+
if (structural / sample.length > JSON_DENSITY_THRESHOLD)
|
|
466
|
+
jsonPenalty = JSON_PENALTY;
|
|
467
|
+
}
|
|
398
468
|
const langPenalty = /[\u00E7\u011F\u0131\u00F6\u015F\u00FC\u00C7\u011E\u0130\u00D6\u015E\u00DC]/.test(text) ? 0.9 : 1;
|
|
399
469
|
const factor = calibration.get(provider, model);
|
|
400
470
|
return Math.ceil(text.length / baseRatio * jsonPenalty * langPenalty * factor);
|
|
@@ -402,7 +472,7 @@ function estimateTokens(text, provider, model, calibration = _fallbackCalibratio
|
|
|
402
472
|
function calibrateFromResponse(estimated, actual, provider, model, calibration = _fallbackCalibration) {
|
|
403
473
|
calibration.calibrate(estimated, actual, provider, model);
|
|
404
474
|
}
|
|
405
|
-
var PROVIDER_MAP, PROVIDER_ALIASES, DEFAULT_CAPS, _fallbackCalibration;
|
|
475
|
+
var PROVIDER_MAP, PROVIDER_ALIASES, DEFAULT_CAPS, _fallbackCalibration, JSON_PENALTY = 0.85, JSON_DENSITY_THRESHOLD = 0.05, JSON_DENSITY_SCAN_CAP = 8192;
|
|
406
476
|
var init_tokens = __esm(() => {
|
|
407
477
|
init_constants();
|
|
408
478
|
PROVIDER_MAP = {
|
|
@@ -740,7 +810,7 @@ import crypto4 from "crypto";
|
|
|
740
810
|
class ToolSupportCache {
|
|
741
811
|
entries = new Map;
|
|
742
812
|
ttlMs;
|
|
743
|
-
constructor(ttlMs =
|
|
813
|
+
constructor(ttlMs = ONE_HOUR_MS) {
|
|
744
814
|
this.ttlMs = ttlMs;
|
|
745
815
|
}
|
|
746
816
|
get(key, now) {
|
|
@@ -846,11 +916,7 @@ var init_services = __esm(() => {
|
|
|
846
916
|
|
|
847
917
|
// src/utils/cache.ts
|
|
848
918
|
import fs3 from "fs";
|
|
849
|
-
import
|
|
850
|
-
import crypto5 from "crypto";
|
|
851
|
-
function fallbackSessionId() {
|
|
852
|
-
return "sc-" + Date.now().toString(36) + "-" + crypto5.randomBytes(4).toString("hex");
|
|
853
|
-
}
|
|
919
|
+
import path5 from "path";
|
|
854
920
|
function cacheOpts(opts, provider, phase, services) {
|
|
855
921
|
if (phase && INTERNAL_PHASES.has(phase)) {
|
|
856
922
|
return { ...opts, cacheRetention: "none" };
|
|
@@ -860,7 +926,7 @@ function cacheOpts(opts, provider, phase, services) {
|
|
|
860
926
|
if (retention === "none") {
|
|
861
927
|
return { ...opts, cacheRetention: "none" };
|
|
862
928
|
}
|
|
863
|
-
return { ...opts, sessionId: services?.compactSessionId ??
|
|
929
|
+
return { ...opts, sessionId: services?.compactSessionId ?? makeCompactSessionId(), cacheRetention: retention };
|
|
864
930
|
}
|
|
865
931
|
function recordMetric(m, services = getDefaultServices()) {
|
|
866
932
|
services.metrics.record(m);
|
|
@@ -966,14 +1032,14 @@ function scheduleExtractionCacheCleanup() {
|
|
|
966
1032
|
_extractionPruneInFlight = true;
|
|
967
1033
|
queueMicrotask(() => {
|
|
968
1034
|
try {
|
|
969
|
-
const dir =
|
|
1035
|
+
const dir = path5.dirname(getCachePath("_"));
|
|
970
1036
|
if (!fs3.existsSync(dir))
|
|
971
1037
|
return;
|
|
972
1038
|
const now = Date.now();
|
|
973
1039
|
for (const name of fs3.readdirSync(dir)) {
|
|
974
|
-
if (!name.startsWith(
|
|
1040
|
+
if (!name.startsWith(EXTRACTION_CACHE_PREFIX) || !name.endsWith(".json"))
|
|
975
1041
|
continue;
|
|
976
|
-
const fp =
|
|
1042
|
+
const fp = path5.join(dir, name);
|
|
977
1043
|
try {
|
|
978
1044
|
const stat = fs3.statSync(fp);
|
|
979
1045
|
if (now - stat.mtimeMs > EXTRACTION_CACHE_PRUNE_MAX_AGE_MS) {
|
|
@@ -1019,9 +1085,9 @@ function mergeExtractions(base, delta, baseMsgCount) {
|
|
|
1019
1085
|
constraints: [...base.constraints, ...offsetConstraints],
|
|
1020
1086
|
topics: [...base.topics, ...offsetTopics],
|
|
1021
1087
|
timeline: [...base.timeline, ...offsetTimeline],
|
|
1022
|
-
mainGoal:
|
|
1023
|
-
lastUserMessages:
|
|
1024
|
-
lastErrors:
|
|
1088
|
+
mainGoal: base.mainGoal ?? delta.mainGoal,
|
|
1089
|
+
lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
|
|
1090
|
+
lastErrors: [...base.lastErrors, ...delta.lastErrors].slice(-3),
|
|
1025
1091
|
messageCount: baseMsgCount + delta.messageCount
|
|
1026
1092
|
};
|
|
1027
1093
|
}
|
|
@@ -1317,6 +1383,7 @@ var init_cache = __esm(() => {
|
|
|
1317
1383
|
init_llm_client();
|
|
1318
1384
|
init_paths();
|
|
1319
1385
|
init_fs();
|
|
1386
|
+
init_constants();
|
|
1320
1387
|
init_id_fingerprint();
|
|
1321
1388
|
init_services();
|
|
1322
1389
|
INTERNAL_PHASES = new Set([
|
|
@@ -1329,8 +1396,8 @@ var init_cache = __esm(() => {
|
|
|
1329
1396
|
"assemble",
|
|
1330
1397
|
"patch"
|
|
1331
1398
|
]);
|
|
1332
|
-
EXTRACTION_CACHE_TTL_MS =
|
|
1333
|
-
EXTRACTION_CACHE_PRUNE_MAX_AGE_MS =
|
|
1399
|
+
EXTRACTION_CACHE_TTL_MS = ONE_HOUR_MS;
|
|
1400
|
+
EXTRACTION_CACHE_PRUNE_MAX_AGE_MS = SEVEN_DAYS_MS;
|
|
1334
1401
|
});
|
|
1335
1402
|
|
|
1336
1403
|
// src/ui/dashboard-format.ts
|
|
@@ -1434,9 +1501,12 @@ var DASHBOARD_PAGE_SIZE = 24;
|
|
|
1434
1501
|
var exports_overlays = {};
|
|
1435
1502
|
__export(exports_overlays, {
|
|
1436
1503
|
showResultScreen: () => showResultScreen,
|
|
1504
|
+
showRestorePicker: () => showRestorePicker,
|
|
1505
|
+
showRestoreAction: () => showRestoreAction,
|
|
1437
1506
|
showProgressOverlay: () => showProgressOverlay,
|
|
1438
1507
|
showMetricsDashboardUI: () => showMetricsDashboardUI,
|
|
1439
1508
|
showCompactUI: () => showCompactUI,
|
|
1509
|
+
showBackupViewer: () => showBackupViewer,
|
|
1440
1510
|
selectProfile: () => selectProfile,
|
|
1441
1511
|
selectModel: () => selectModel,
|
|
1442
1512
|
renderTokenBar: () => renderTokenBar,
|
|
@@ -1444,7 +1514,7 @@ __export(exports_overlays, {
|
|
|
1444
1514
|
});
|
|
1445
1515
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
1446
1516
|
import { Container, Key, matchesKey, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
1447
|
-
import
|
|
1517
|
+
import path6 from "path";
|
|
1448
1518
|
function renderContextBar(theme, pct, tokens, barLen = 24) {
|
|
1449
1519
|
const clamped = Math.min(Math.max(pct, 0), 100);
|
|
1450
1520
|
const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
|
|
@@ -1627,7 +1697,7 @@ async function showResultScreen(ctx, details, extraction, services) {
|
|
|
1627
1697
|
const f = modFiles[i];
|
|
1628
1698
|
const fc = extraction.modifiedFiles.find((e) => e.path === f);
|
|
1629
1699
|
const count = fc ? " (" + fc.toolCalls + "x)" : "";
|
|
1630
|
-
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text",
|
|
1700
|
+
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path6.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
|
|
1631
1701
|
}
|
|
1632
1702
|
if (modFiles.length > maxShow) {
|
|
1633
1703
|
c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
|
|
@@ -1650,7 +1720,7 @@ async function showResultScreen(ctx, details, extraction, services) {
|
|
|
1650
1720
|
c.addChild(new Text(theme.fg("success", " \u2705 All facts verified \u2014 no gaps detected"), 0, 0));
|
|
1651
1721
|
} else if (details.gaps.length > 0) {
|
|
1652
1722
|
c.addChild(new Text(theme.fg("warning", " \u26A0\uFE0F " + details.gaps.length + " gap(s) patched:"), 0, 0));
|
|
1653
|
-
for (const g of details.gaps.slice(0,
|
|
1723
|
+
for (const g of details.gaps.slice(0, TRUNC.RESULT_GAPS)) {
|
|
1654
1724
|
c.addChild(new Text(theme.fg("dim", " \u2022 " + g), 0, 0));
|
|
1655
1725
|
}
|
|
1656
1726
|
}
|
|
@@ -1797,7 +1867,123 @@ async function showCompactUI(ctx, opts) {
|
|
|
1797
1867
|
return null;
|
|
1798
1868
|
return { model: selectedModel, profile: selectedProfile };
|
|
1799
1869
|
}
|
|
1870
|
+
async function showRestorePicker(ctx, backups) {
|
|
1871
|
+
const items = backups.map((b) => ({
|
|
1872
|
+
value: b.path,
|
|
1873
|
+
label: new Date(b.date).toLocaleString() + " \xB7 " + Math.max(1, Math.round(b.sizeBytes / 1024)) + "KB",
|
|
1874
|
+
description: b.sessionId.slice(0, TRUNC.SESSION_ID_DISPLAY)
|
|
1875
|
+
}));
|
|
1876
|
+
return await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
1877
|
+
const c = new Container;
|
|
1878
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1879
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Smart Compact \u2014 Restore")), 1, 0));
|
|
1880
|
+
c.addChild(new Text(theme.fg("dim", " Pick a backup to view its pre-compaction content"), 0, 0));
|
|
1881
|
+
c.addChild(new Text("", 0, 0));
|
|
1882
|
+
const sel = new SelectList(items, Math.min(items.length, 12), {
|
|
1883
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
1884
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
1885
|
+
description: (t) => theme.fg("muted", t),
|
|
1886
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
1887
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
1888
|
+
});
|
|
1889
|
+
sel.onSelect = (item) => done(item.value);
|
|
1890
|
+
sel.onCancel = () => done(null);
|
|
1891
|
+
c.addChild(sel);
|
|
1892
|
+
c.addChild(new Text("", 0, 0));
|
|
1893
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter view \xB7 esc cancel"), 0, 0));
|
|
1894
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1895
|
+
return {
|
|
1896
|
+
render: (w) => c.render(w),
|
|
1897
|
+
invalidate: () => c.invalidate(),
|
|
1898
|
+
handleInput: (d) => {
|
|
1899
|
+
sel.handleInput(d);
|
|
1900
|
+
tui.requestRender();
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
async function showBackupViewer(ctx, content, fp) {
|
|
1906
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
1907
|
+
const lines = content.split(`
|
|
1908
|
+
`);
|
|
1909
|
+
const pageSize = 40;
|
|
1910
|
+
let scroll = 0;
|
|
1911
|
+
const maxScroll = Math.max(0, lines.length - pageSize);
|
|
1912
|
+
return {
|
|
1913
|
+
render: (w) => {
|
|
1914
|
+
const out = [
|
|
1915
|
+
truncateToWidth(theme.fg("accent", theme.bold(" \u21A9 Restored backup")) + theme.fg("dim", " \xB7 " + lines.length + " lines \xB7 " + Math.max(1, Math.round(content.length / 1024)) + "KB"), w),
|
|
1916
|
+
truncateToWidth(theme.fg("dim", " " + fp), w),
|
|
1917
|
+
truncateToWidth(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, w))), w)
|
|
1918
|
+
];
|
|
1919
|
+
for (const line of lines.slice(scroll, scroll + pageSize)) {
|
|
1920
|
+
out.push(truncateToWidth(theme.fg("text", line), w));
|
|
1921
|
+
}
|
|
1922
|
+
if (lines.length > pageSize) {
|
|
1923
|
+
out.push(truncateToWidth(theme.fg("dim", " showing " + (scroll + 1) + "\u2013" + Math.min(lines.length, scroll + pageSize) + " of " + lines.length), w));
|
|
1924
|
+
}
|
|
1925
|
+
out.push("", truncateToWidth(theme.fg("dim", " \u2191\u2193 scroll \xB7 pgup/pgdn \xB7 home/end \xB7 esc/q close"), w));
|
|
1926
|
+
return out;
|
|
1927
|
+
},
|
|
1928
|
+
invalidate: () => {},
|
|
1929
|
+
handleInput: (data) => {
|
|
1930
|
+
if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
|
|
1931
|
+
done(undefined);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
if (matchesKey(data, Key.home))
|
|
1935
|
+
scroll = 0;
|
|
1936
|
+
else if (matchesKey(data, Key.end))
|
|
1937
|
+
scroll = maxScroll;
|
|
1938
|
+
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
1939
|
+
scroll = Math.max(0, scroll - pageSize);
|
|
1940
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
1941
|
+
scroll = Math.min(maxScroll, scroll + pageSize);
|
|
1942
|
+
else if (keybindings.matches(data, "tui.select.up"))
|
|
1943
|
+
scroll = Math.max(0, scroll - 1);
|
|
1944
|
+
else if (keybindings.matches(data, "tui.select.down"))
|
|
1945
|
+
scroll = Math.min(maxScroll, scroll + 1);
|
|
1946
|
+
tui.requestRender();
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
}, { overlay: true, overlayOptions: { width: "85%", anchor: "center", maxHeight: "85%" } });
|
|
1950
|
+
}
|
|
1951
|
+
async function showRestoreAction(ctx, backupPath) {
|
|
1952
|
+
const items = [
|
|
1953
|
+
{ value: "view", label: "View content", description: "Read the pre-compaction conversation" },
|
|
1954
|
+
{ value: "restore", label: "Restore into a new session", description: "Fork from here + inject this backup as context" }
|
|
1955
|
+
];
|
|
1956
|
+
return await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
1957
|
+
const c = new Container;
|
|
1958
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1959
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Restore action")), 1, 0));
|
|
1960
|
+
c.addChild(new Text(theme.fg("dim", " " + backupPath), 0, 0));
|
|
1961
|
+
c.addChild(new Text("", 0, 0));
|
|
1962
|
+
const sel = new SelectList(items, 2, {
|
|
1963
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
1964
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
1965
|
+
description: (t) => theme.fg("muted", t),
|
|
1966
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
1967
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
1968
|
+
});
|
|
1969
|
+
sel.onSelect = (item) => done(item.value);
|
|
1970
|
+
sel.onCancel = () => done(null);
|
|
1971
|
+
c.addChild(sel);
|
|
1972
|
+
c.addChild(new Text("", 0, 0));
|
|
1973
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter select \xB7 esc cancel"), 0, 0));
|
|
1974
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1975
|
+
return {
|
|
1976
|
+
render: (w) => c.render(w),
|
|
1977
|
+
invalidate: () => c.invalidate(),
|
|
1978
|
+
handleInput: (d) => {
|
|
1979
|
+
sel.handleInput(d);
|
|
1980
|
+
tui.requestRender();
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1800
1985
|
var init_overlays = __esm(() => {
|
|
1986
|
+
init_constants();
|
|
1801
1987
|
init_cache();
|
|
1802
1988
|
init_tokens();
|
|
1803
1989
|
});
|
|
@@ -1810,1081 +1996,1161 @@ init_constants();
|
|
|
1810
1996
|
init_logger();
|
|
1811
1997
|
init_paths();
|
|
1812
1998
|
init_fs();
|
|
1813
|
-
init_constants();
|
|
1814
1999
|
import fs2 from "fs";
|
|
1815
|
-
import
|
|
2000
|
+
import path4 from "path";
|
|
1816
2001
|
import crypto2 from "crypto";
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
delete sc.autoTrigger;
|
|
1827
|
-
}
|
|
1828
|
-
if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
|
|
1829
|
-
warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
|
|
1830
|
-
delete sc.backupEnabled;
|
|
1831
|
-
}
|
|
1832
|
-
if ("summaryModel" in sc && sc.summaryModel !== null && typeof sc.summaryModel !== "string") {
|
|
1833
|
-
warn("smart-compact config: summaryModel must be string|null, got " + typeof sc.summaryModel);
|
|
1834
|
-
delete sc.summaryModel;
|
|
1835
|
-
}
|
|
1836
|
-
if ("segmentationModel" in sc && sc.segmentationModel !== null && typeof sc.segmentationModel !== "string") {
|
|
1837
|
-
warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
|
|
1838
|
-
delete sc.segmentationModel;
|
|
1839
|
-
}
|
|
1840
|
-
if ("profiles" in sc) {
|
|
1841
|
-
if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
|
|
1842
|
-
warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
1843
|
-
delete sc.profiles;
|
|
1844
|
-
} else {
|
|
1845
|
-
const profiles = sc.profiles;
|
|
1846
|
-
for (const [profileName, value] of Object.entries(profiles)) {
|
|
1847
|
-
if (!VALID_PROFILES.includes(profileName)) {
|
|
1848
|
-
warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
|
|
1849
|
-
delete profiles[profileName];
|
|
1850
|
-
continue;
|
|
1851
|
-
}
|
|
1852
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1853
|
-
warn("smart-compact config: profile '" + profileName + "' must be an object.");
|
|
1854
|
-
delete profiles[profileName];
|
|
1855
|
-
continue;
|
|
1856
|
-
}
|
|
1857
|
-
const profileCfg = value;
|
|
1858
|
-
for (const [key, raw] of Object.entries(profileCfg)) {
|
|
1859
|
-
if (!PROFILE_NUMERIC_KEYS.includes(key)) {
|
|
1860
|
-
warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
|
|
1861
|
-
delete profileCfg[key];
|
|
1862
|
-
continue;
|
|
1863
|
-
}
|
|
1864
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
|
|
1865
|
-
warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
|
|
1866
|
-
delete profileCfg[key];
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
if ("autoTriggerTimeoutMs" in sc) {
|
|
1873
|
-
const v = sc.autoTriggerTimeoutMs;
|
|
1874
|
-
if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
|
|
1875
|
-
warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
|
|
1876
|
-
delete sc.autoTriggerTimeoutMs;
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
if ("minContextPercent" in sc) {
|
|
1880
|
-
const v = sc.minContextPercent;
|
|
1881
|
-
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
|
|
1882
|
-
warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
|
|
1883
|
-
delete sc.minContextPercent;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
2002
|
+
|
|
2003
|
+
// src/utils/extraction.ts
|
|
2004
|
+
init_constants();
|
|
2005
|
+
init_tokens();
|
|
2006
|
+
import path3 from "path";
|
|
2007
|
+
|
|
2008
|
+
// src/utils/type-guards.ts
|
|
2009
|
+
function isTextBlock(c) {
|
|
2010
|
+
return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
|
|
1886
2011
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
var _cfgPath = null;
|
|
1890
|
-
function loadConfig() {
|
|
1891
|
-
try {
|
|
1892
|
-
const p = settingsFile();
|
|
1893
|
-
const stat = fs2.statSync(p);
|
|
1894
|
-
if (_cfg && _cfgPath === p && stat.mtimeMs === _cfgMtime)
|
|
1895
|
-
return _cfg;
|
|
1896
|
-
const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
1897
|
-
const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
|
|
1898
|
-
validateSmartCompactConfig(sc);
|
|
1899
|
-
const merged = { ...DEFAULT_CONFIG, ...sc };
|
|
1900
|
-
if (sc.profiles)
|
|
1901
|
-
merged.profiles = { ...PROFILES, ...sc.profiles };
|
|
1902
|
-
if (!merged.backupDir)
|
|
1903
|
-
merged.backupDir = defaultBackupDir();
|
|
1904
|
-
_cfg = merged;
|
|
1905
|
-
_cfgMtime = stat.mtimeMs;
|
|
1906
|
-
_cfgPath = p;
|
|
1907
|
-
return _cfg;
|
|
1908
|
-
} catch (e) {
|
|
1909
|
-
debug("loadConfig: settings.json not found or unreadable, using defaults", e);
|
|
1910
|
-
const fallback = { ...DEFAULT_CONFIG, backupDir: defaultBackupDir() };
|
|
1911
|
-
_cfg = fallback;
|
|
1912
|
-
_cfgPath = null;
|
|
1913
|
-
return fallback;
|
|
1914
|
-
}
|
|
2012
|
+
function isToolCallBlock(c) {
|
|
2013
|
+
return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
|
|
1915
2014
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const full = path3.join(dir, name);
|
|
1921
|
-
try {
|
|
1922
|
-
return { full, mtimeMs: fs2.statSync(full).mtimeMs };
|
|
1923
|
-
} catch {
|
|
1924
|
-
return null;
|
|
1925
|
-
}
|
|
1926
|
-
}).filter((v) => v !== null);
|
|
1927
|
-
const now = Date.now();
|
|
1928
|
-
const overAge = entries.filter((e) => now - e.mtimeMs > BACKUP_MAX_AGE_MS);
|
|
1929
|
-
const sorted = entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1930
|
-
const overCount = sorted.slice(BACKUP_MAX_FILES);
|
|
1931
|
-
const toRemove = new Set([...overAge, ...overCount].map((e) => e.full));
|
|
1932
|
-
for (const full of toRemove) {
|
|
1933
|
-
try {
|
|
1934
|
-
fs2.unlinkSync(full);
|
|
1935
|
-
} catch (e) {
|
|
1936
|
-
debug("prunePass unlink failed", e);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
} catch (e) {
|
|
1940
|
-
debug("prunePass scan failed", e);
|
|
1941
|
-
}
|
|
2015
|
+
function getToolCallNames(content) {
|
|
2016
|
+
if (!Array.isArray(content))
|
|
2017
|
+
return [];
|
|
2018
|
+
return content.filter(isToolCallBlock).map((b) => b.name);
|
|
1942
2019
|
}
|
|
1943
|
-
function
|
|
1944
|
-
if (
|
|
1945
|
-
return;
|
|
1946
|
-
|
|
1947
|
-
queueMicrotask(() => {
|
|
1948
|
-
try {
|
|
1949
|
-
prunePass(dir);
|
|
1950
|
-
} finally {
|
|
1951
|
-
_pruneInFlight.delete(dir);
|
|
1952
|
-
}
|
|
1953
|
-
});
|
|
2020
|
+
function filterToolCalls(content) {
|
|
2021
|
+
if (!Array.isArray(content))
|
|
2022
|
+
return [];
|
|
2023
|
+
return content.filter(isToolCallBlock);
|
|
1954
2024
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
return null;
|
|
1960
|
-
const dir = cfg.backupDir;
|
|
1961
|
-
ensureDir(dir);
|
|
1962
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1963
|
-
const hash = crypto2.createHash("sha256").update(convText).digest("hex").slice(0, 8);
|
|
1964
|
-
const fp = path3.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
|
|
1965
|
-
atomicWriteFileSync(fp, `# Smart Compact Backup
|
|
1966
|
-
# Date: ` + new Date().toISOString() + `
|
|
1967
|
-
# Session: ` + sessionId + `
|
|
1968
|
-
|
|
1969
|
-
` + convText);
|
|
1970
|
-
schedulePruneBackups(dir);
|
|
1971
|
-
return fp;
|
|
1972
|
-
} catch (e) {
|
|
1973
|
-
warn("backupConversation failed", e);
|
|
1974
|
-
return null;
|
|
1975
|
-
}
|
|
2025
|
+
var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
|
|
2026
|
+
var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
|
|
2027
|
+
function isStringArray(v) {
|
|
2028
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
1976
2029
|
}
|
|
1977
|
-
function
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
if (!
|
|
1984
|
-
return
|
|
1985
|
-
|
|
1986
|
-
|
|
2030
|
+
function isValidSmartCompactDetails(d) {
|
|
2031
|
+
if (!d || typeof d !== "object")
|
|
2032
|
+
return false;
|
|
2033
|
+
const r = d;
|
|
2034
|
+
if (!isStringArray(r.modifiedFiles))
|
|
2035
|
+
return false;
|
|
2036
|
+
if (!isStringArray(r.readFiles))
|
|
2037
|
+
return false;
|
|
2038
|
+
if (!isStringArray(r.topics))
|
|
2039
|
+
return false;
|
|
2040
|
+
if (typeof r.method !== "string" || !KNOWN_METHODS.has(r.method))
|
|
2041
|
+
return false;
|
|
2042
|
+
if (typeof r.profile !== "string" || !KNOWN_PROFILES.has(r.profile))
|
|
2043
|
+
return false;
|
|
2044
|
+
if (typeof r.qualityScore !== "number" || !Number.isFinite(r.qualityScore))
|
|
2045
|
+
return false;
|
|
2046
|
+
if (typeof r.totalMessages !== "number" || !Number.isFinite(r.totalMessages))
|
|
2047
|
+
return false;
|
|
2048
|
+
if (r.gaps !== undefined && !isStringArray(r.gaps))
|
|
2049
|
+
return false;
|
|
2050
|
+
if (r.verified !== undefined && typeof r.verified !== "boolean")
|
|
2051
|
+
return false;
|
|
2052
|
+
if (r.backupPath !== undefined && r.backupPath !== null && typeof r.backupPath !== "string")
|
|
2053
|
+
return false;
|
|
2054
|
+
return true;
|
|
1987
2055
|
}
|
|
1988
|
-
function
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2056
|
+
function sanitizeSmartCompactDetails(d) {
|
|
2057
|
+
if (isValidSmartCompactDetails(d))
|
|
2058
|
+
return d;
|
|
2059
|
+
if (!d || typeof d !== "object")
|
|
2060
|
+
return null;
|
|
2061
|
+
const r = d;
|
|
2062
|
+
if (!isStringArray(r.modifiedFiles) || !isStringArray(r.readFiles) || !isStringArray(r.topics))
|
|
2063
|
+
return null;
|
|
2064
|
+
const repaired = {
|
|
2065
|
+
method: KNOWN_METHODS.has(r.method) ? r.method : "heuristic",
|
|
2066
|
+
chunkCount: typeof r.chunkCount === "number" ? r.chunkCount : 0,
|
|
2067
|
+
topics: r.topics,
|
|
2068
|
+
readFiles: r.readFiles,
|
|
2069
|
+
modifiedFiles: r.modifiedFiles,
|
|
2070
|
+
totalMessages: typeof r.totalMessages === "number" ? r.totalMessages : 0,
|
|
2071
|
+
totalTokensSummarized: typeof r.totalTokensSummarized === "number" ? r.totalTokensSummarized : 0,
|
|
2072
|
+
llmCalls: typeof r.llmCalls === "number" ? r.llmCalls : 0,
|
|
2073
|
+
profile: KNOWN_PROFILES.has(r.profile) ? r.profile : "balanced",
|
|
2074
|
+
backupPath: typeof r.backupPath === "string" ? r.backupPath : null,
|
|
2075
|
+
tokensSaved: typeof r.tokensSaved === "number" ? r.tokensSaved : 0,
|
|
2076
|
+
verified: typeof r.verified === "boolean" ? r.verified : false,
|
|
2077
|
+
gaps: isStringArray(r.gaps) ? r.gaps : [],
|
|
2078
|
+
explorationRounds: typeof r.explorationRounds === "number" ? r.explorationRounds : 0,
|
|
2079
|
+
explorationBoundaries: typeof r.explorationBoundaries === "number" ? r.explorationBoundaries : 0,
|
|
2080
|
+
model: typeof r.model === "string" ? r.model : "unknown",
|
|
2081
|
+
qualityScore: typeof r.qualityScore === "number" ? r.qualityScore : 0,
|
|
2082
|
+
tokensBefore: typeof r.tokensBefore === "number" ? r.tokensBefore : 0
|
|
2083
|
+
};
|
|
2084
|
+
return isValidSmartCompactDetails(repaired) ? repaired : null;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/utils/file-needles.ts
|
|
2088
|
+
var GENERIC_BASENAMES = new Set([
|
|
2089
|
+
"index.ts",
|
|
2090
|
+
"index.js",
|
|
2091
|
+
"index.tsx",
|
|
2092
|
+
"index.jsx",
|
|
2093
|
+
"types.ts",
|
|
2094
|
+
"helpers.ts",
|
|
2095
|
+
"utils.ts",
|
|
2096
|
+
"main.ts",
|
|
2097
|
+
"main.js",
|
|
2098
|
+
"mod.rs",
|
|
2099
|
+
"lib.rs",
|
|
2100
|
+
"__init__.py"
|
|
2101
|
+
]);
|
|
2102
|
+
var MIN_BARE_BASENAME_LEN = 5;
|
|
2103
|
+
function buildPathNeedles(filePath) {
|
|
2104
|
+
const parts = filePath.toLowerCase().split("/").filter(Boolean);
|
|
2105
|
+
if (parts.length === 0)
|
|
2106
|
+
return [];
|
|
2107
|
+
const needles = [];
|
|
2108
|
+
const basename = parts[parts.length - 1];
|
|
2109
|
+
if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
|
|
2110
|
+
needles.push(basename);
|
|
1999
2111
|
}
|
|
2000
|
-
|
|
2112
|
+
for (let j = parts.length - 2;j >= 0; j--) {
|
|
2113
|
+
needles.push(parts.slice(j).join("/"));
|
|
2114
|
+
}
|
|
2115
|
+
return needles;
|
|
2001
2116
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2117
|
+
|
|
2118
|
+
// src/domain/tool-semantics.ts
|
|
2119
|
+
var PATH_KEYS = ["path", "file_path", "filePath", "filename", "file"];
|
|
2120
|
+
var PAYLOAD_KEYS = ["content", "newText", "oldText", "edits", "patch", "replacement"];
|
|
2121
|
+
var COMMAND_KEYS = ["command", "cmd", "script"];
|
|
2122
|
+
function hasPresent(args, keys) {
|
|
2123
|
+
return keys.some((k) => args[k] != null);
|
|
2124
|
+
}
|
|
2125
|
+
function extractToolPath(args) {
|
|
2126
|
+
if (!args || typeof args !== "object")
|
|
2127
|
+
return;
|
|
2128
|
+
const a = args;
|
|
2129
|
+
for (const k of PATH_KEYS) {
|
|
2130
|
+
const v = a[k];
|
|
2131
|
+
if (typeof v === "string" && v.length > 0)
|
|
2132
|
+
return v;
|
|
2133
|
+
}
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
function classifyTool(args) {
|
|
2137
|
+
if (!args || typeof args !== "object")
|
|
2138
|
+
return "other";
|
|
2139
|
+
const a = args;
|
|
2140
|
+
const hasPath = extractToolPath(a) !== undefined;
|
|
2141
|
+
const hasPayload = hasPresent(a, PAYLOAD_KEYS);
|
|
2142
|
+
const hasCommand = hasPresent(a, COMMAND_KEYS);
|
|
2143
|
+
if (hasPath && hasPayload)
|
|
2144
|
+
return "mutates";
|
|
2145
|
+
if (hasCommand)
|
|
2146
|
+
return "executes";
|
|
2147
|
+
if (hasPath)
|
|
2148
|
+
return "accesses";
|
|
2149
|
+
return "other";
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/utils/extraction.ts
|
|
2153
|
+
var TRUNCATE_RE = /\u2026\u2702\d+$/;
|
|
2154
|
+
function isTruncated(content) {
|
|
2155
|
+
return TRUNCATE_RE.test(extractText(content));
|
|
2156
|
+
}
|
|
2157
|
+
var DELETE_RESULT_RE = /\b(?:deleted|removed|unlinked)\b/i;
|
|
2158
|
+
function flattenToolCallBlock(b) {
|
|
2159
|
+
if (!isToolCallBlock(b))
|
|
2160
|
+
return [];
|
|
2161
|
+
if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
|
|
2162
|
+
return b.arguments.tool_uses.map((u) => {
|
|
2163
|
+
const recipient = u?.recipient_name ?? "";
|
|
2164
|
+
return {
|
|
2165
|
+
name: recipient.replace(/^functions\./, ""),
|
|
2166
|
+
id: u?.id ?? undefined,
|
|
2167
|
+
arguments: u?.parameters ?? {}
|
|
2168
|
+
};
|
|
2169
|
+
});
|
|
2011
2170
|
}
|
|
2012
|
-
return
|
|
2171
|
+
return [{ name: b.name, id: b.id, arguments: b.arguments }];
|
|
2013
2172
|
}
|
|
2014
|
-
function
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2173
|
+
function extractText(content) {
|
|
2174
|
+
if (typeof content === "string")
|
|
2175
|
+
return content;
|
|
2176
|
+
if (!Array.isArray(content))
|
|
2177
|
+
return "";
|
|
2178
|
+
return content.map((b) => {
|
|
2179
|
+
if (typeof b === "string")
|
|
2180
|
+
return b;
|
|
2181
|
+
if (isTextBlock(b))
|
|
2182
|
+
return b.text;
|
|
2183
|
+
return "";
|
|
2184
|
+
}).join("");
|
|
2185
|
+
}
|
|
2186
|
+
function mediaKind(type, mime) {
|
|
2187
|
+
const s = (type + " " + (mime ?? "")).toLowerCase();
|
|
2188
|
+
if (/image|input_image|image_url/.test(s))
|
|
2189
|
+
return "image";
|
|
2190
|
+
if (/audio/.test(s))
|
|
2191
|
+
return "audio";
|
|
2192
|
+
if (/video/.test(s))
|
|
2193
|
+
return "video";
|
|
2194
|
+
if (/file|document|pdf|attachment/.test(s))
|
|
2195
|
+
return "file";
|
|
2196
|
+
return "unknown";
|
|
2197
|
+
}
|
|
2198
|
+
function extractMediaAttachments(msgs) {
|
|
2199
|
+
const out = [];
|
|
2200
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2201
|
+
const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
|
|
2202
|
+
for (const b of blocks) {
|
|
2203
|
+
if (!b || typeof b !== "object")
|
|
2204
|
+
continue;
|
|
2205
|
+
const rec = b;
|
|
2206
|
+
const type = String(rec.type ?? "");
|
|
2207
|
+
if (type === "text" || type === "toolCall" || type === "tool_use")
|
|
2208
|
+
continue;
|
|
2209
|
+
const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
|
|
2210
|
+
const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
|
|
2211
|
+
const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
|
|
2212
|
+
const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
|
|
2213
|
+
const kind = mediaKind(type, mimeType);
|
|
2214
|
+
if (kind !== "unknown" || source || mimeType || name) {
|
|
2215
|
+
out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
|
|
2022
2216
|
}
|
|
2023
2217
|
}
|
|
2024
2218
|
}
|
|
2025
|
-
|
|
2026
|
-
return adjusted;
|
|
2027
|
-
const last = msgs[adjusted - 1];
|
|
2028
|
-
const first = msgs[adjusted];
|
|
2029
|
-
if (last && first) {
|
|
2030
|
-
const getText = (msg) => {
|
|
2031
|
-
const m = msg;
|
|
2032
|
-
const c = m?.content;
|
|
2033
|
-
if (typeof c === "string")
|
|
2034
|
-
return c;
|
|
2035
|
-
if (Array.isArray(c))
|
|
2036
|
-
return c.map((b) => {
|
|
2037
|
-
if (typeof b === "string")
|
|
2038
|
-
return b;
|
|
2039
|
-
if (typeof b === "object" && b !== null && b.type === "text")
|
|
2040
|
-
return b.text ?? "";
|
|
2041
|
-
return "";
|
|
2042
|
-
}).join("");
|
|
2043
|
-
return "";
|
|
2044
|
-
};
|
|
2045
|
-
const lastText = getText(last.message).toLowerCase();
|
|
2046
|
-
const keptText = getText(first.message).toLowerCase();
|
|
2047
|
-
const fileRe = /(?:path|file)=["']([^"']+)["']/g;
|
|
2048
|
-
const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
2049
|
-
fileRe.lastIndex = 0;
|
|
2050
|
-
const keptFiles = new Set([...keptText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
2051
|
-
if ([...lastFiles].filter((f) => keptFiles.has(f)).length > 0)
|
|
2052
|
-
return adjusted - 1;
|
|
2053
|
-
}
|
|
2054
|
-
return adjusted;
|
|
2219
|
+
return out;
|
|
2055
2220
|
}
|
|
2056
|
-
function
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2221
|
+
function buildToolCallIndex(msgs) {
|
|
2222
|
+
const idx = new Map;
|
|
2223
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2224
|
+
const m = msgs[i];
|
|
2225
|
+
if (m.role !== "assistant")
|
|
2226
|
+
continue;
|
|
2227
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
2228
|
+
for (const b of blocks) {
|
|
2229
|
+
if (!isToolCallBlock(b))
|
|
2230
|
+
continue;
|
|
2231
|
+
if (b.id) {
|
|
2232
|
+
idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
|
|
2062
2233
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
for (
|
|
2066
|
-
const
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2234
|
+
if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
|
|
2235
|
+
const nested = flattenToolCallBlock(b);
|
|
2236
|
+
for (let t = 0;t < nested.length; t++) {
|
|
2237
|
+
const tool = nested[t];
|
|
2238
|
+
const syntheticId = b.id ? b.id + "_" + t : "mtu_" + i + "_" + t;
|
|
2239
|
+
idx.set(tool.id || syntheticId, { name: tool.name, arguments: tool.arguments, msgIndex: i });
|
|
2070
2240
|
}
|
|
2071
2241
|
}
|
|
2072
2242
|
}
|
|
2073
2243
|
}
|
|
2244
|
+
return idx;
|
|
2074
2245
|
}
|
|
2075
|
-
function
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2246
|
+
function trackFileOps(msgs, _tcIdx) {
|
|
2247
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
2248
|
+
const modMap = new Map;
|
|
2249
|
+
const readSet = new Set;
|
|
2250
|
+
const delSet = new Set;
|
|
2079
2251
|
for (let i = 0;i < msgs.length; i++) {
|
|
2080
|
-
const m = msgs[i]
|
|
2081
|
-
if (m
|
|
2252
|
+
const m = msgs[i];
|
|
2253
|
+
if (m.role !== "toolResult" || m.isError)
|
|
2082
2254
|
continue;
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2255
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
2256
|
+
if (!tc)
|
|
2257
|
+
continue;
|
|
2258
|
+
const filePath = extractToolPath(tc.arguments);
|
|
2259
|
+
if (!filePath)
|
|
2260
|
+
continue;
|
|
2261
|
+
const cls = classifyTool(tc.arguments);
|
|
2262
|
+
if (cls === "mutates") {
|
|
2263
|
+
const resultText = extractText(m.content);
|
|
2264
|
+
if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
|
|
2265
|
+
const existing = modMap.get(filePath);
|
|
2266
|
+
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
2267
|
+
}
|
|
2268
|
+
} else if (cls === "accesses") {
|
|
2269
|
+
if (DELETE_RESULT_RE.test(extractText(m.content)))
|
|
2270
|
+
delSet.add(filePath);
|
|
2271
|
+
else
|
|
2272
|
+
readSet.add(filePath);
|
|
2094
2273
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2274
|
+
}
|
|
2275
|
+
return {
|
|
2276
|
+
modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
|
|
2277
|
+
read: [...readSet],
|
|
2278
|
+
deleted: [...delSet]
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function catalogErrors(msgs, _tcIdx) {
|
|
2282
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
2283
|
+
const errors = [];
|
|
2284
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2285
|
+
const m = msgs[i];
|
|
2286
|
+
if (m.role !== "toolResult")
|
|
2287
|
+
continue;
|
|
2288
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
2289
|
+
if (m.isError) {
|
|
2290
|
+
errors.push({ index: i, tool: tc?.name ?? "unknown", message: extractText(m.content).slice(0, TRUNC.ERROR_DETAIL), retryAttempted: false, resolved: false });
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
if (tc && classifyTool(tc.arguments) === "executes") {
|
|
2294
|
+
const txt = extractText(m.content);
|
|
2295
|
+
if (LIKELY_ERROR_RE.test(txt) && txt.length < ERROR_SCAN_MAX_LEN) {
|
|
2296
|
+
errors.push({ index: i, tool: tc.name, message: txt.slice(0, TRUNC.MESSAGE), retryAttempted: false, resolved: false });
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
for (const err of errors) {
|
|
2301
|
+
for (let j = err.index + 1;j < Math.min(msgs.length, err.index + ERROR_RETRY_WINDOW); j++) {
|
|
2302
|
+
if (msgs[j]?.role !== "assistant")
|
|
2099
2303
|
continue;
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2304
|
+
const rawBlocks = msgs[j]?.content;
|
|
2305
|
+
const blocks = Array.isArray(rawBlocks) ? rawBlocks : [];
|
|
2306
|
+
const retryTool = blocks.flatMap((b) => flattenToolCallBlock(b)).find((t) => t.name === err.tool);
|
|
2307
|
+
if (!retryTool)
|
|
2102
2308
|
continue;
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2309
|
+
err.retryAttempted = true;
|
|
2310
|
+
for (let k = j + 1;k < Math.min(msgs.length, j + ERROR_RESOLVE_WINDOW); k++) {
|
|
2311
|
+
const mk = msgs[k];
|
|
2312
|
+
if (mk?.role !== "toolResult" || mk.isError)
|
|
2313
|
+
continue;
|
|
2314
|
+
const resolved = retryTool.id != null ? mk.toolCallId === retryTool.id : tcIdx.get(mk.toolCallId ?? "")?.name === err.tool;
|
|
2315
|
+
if (resolved) {
|
|
2316
|
+
err.resolved = true;
|
|
2317
|
+
break;
|
|
2318
|
+
}
|
|
2108
2319
|
}
|
|
2320
|
+
break;
|
|
2109
2321
|
}
|
|
2110
2322
|
}
|
|
2111
|
-
return
|
|
2112
|
-
}
|
|
2113
|
-
function extractUserNote(args) {
|
|
2114
|
-
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
2115
|
-
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2116
|
-
const isOptionToken = (t) => t.startsWith("--") || SKIP.has(t.toLowerCase()) || /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t);
|
|
2117
|
-
const nonFlags = tokens.filter((t) => !isOptionToken(t));
|
|
2118
|
-
return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
|
|
2323
|
+
return errors;
|
|
2119
2324
|
}
|
|
2120
|
-
function
|
|
2121
|
-
const
|
|
2122
|
-
|
|
2123
|
-
for (const
|
|
2124
|
-
if (
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2325
|
+
function extractDecisions(msgs, _tcIdx) {
|
|
2326
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
2327
|
+
const decisions = [];
|
|
2328
|
+
for (const [id, tc] of tcIdx) {
|
|
2329
|
+
if (tc.name !== "ask_user")
|
|
2330
|
+
continue;
|
|
2331
|
+
const args = tc.arguments;
|
|
2332
|
+
const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
|
|
2333
|
+
if (!question)
|
|
2334
|
+
continue;
|
|
2335
|
+
for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
|
|
2336
|
+
if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
|
|
2337
|
+
decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, TRUNC.DECISION_SUMMARY), userResponse: extractText(msgs[i].content).slice(0, TRUNC.USER_RESPONSE) });
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2128
2340
|
}
|
|
2129
|
-
batch.push(ch);
|
|
2130
|
-
bt += ch.tokenEstimate;
|
|
2131
2341
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
return new Map;
|
|
2140
|
-
const weights = summaries.map((s, i) => {
|
|
2141
|
-
let w = 1;
|
|
2142
|
-
if (s.priority === "critical")
|
|
2143
|
-
w *= 2;
|
|
2144
|
-
else if (s.priority === "high")
|
|
2145
|
-
w *= 1.5;
|
|
2146
|
-
else if (s.priority === "low")
|
|
2147
|
-
w *= 0.6;
|
|
2148
|
-
const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
|
|
2149
|
-
w *= 1 + errorKeywords * 0.2;
|
|
2150
|
-
const recency = (i + 1) / n;
|
|
2151
|
-
w *= 0.6 + recency * 0.4;
|
|
2152
|
-
if (s.keyDecisions.length > 0)
|
|
2153
|
-
w *= 1.3;
|
|
2154
|
-
return w;
|
|
2155
|
-
});
|
|
2156
|
-
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
2157
|
-
const baseTokensPerTopic = Math.floor(totalBudget / n);
|
|
2158
|
-
const budgetMap = new Map;
|
|
2159
|
-
for (let i = 0;i < summaries.length; i++) {
|
|
2160
|
-
const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
|
|
2161
|
-
budgetMap.set(summaries[i].topic, Math.max(200, allocated));
|
|
2342
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2343
|
+
if (msgs[i]?.role !== "user")
|
|
2344
|
+
continue;
|
|
2345
|
+
const txt = extractText(msgs[i].content);
|
|
2346
|
+
if (CHOICE_RE.test(txt)) {
|
|
2347
|
+
decisions.push({ index: i, type: "implicit", summary: txt.slice(0, TRUNC.DECISION_SUMMARY) });
|
|
2348
|
+
}
|
|
2162
2349
|
}
|
|
2163
|
-
return
|
|
2164
|
-
}
|
|
2165
|
-
function preProcessSummaries(summaries, budgetTokens) {
|
|
2166
|
-
const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
|
|
2167
|
-
return {
|
|
2168
|
-
decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
|
|
2169
|
-
modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
|
|
2170
|
-
read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
|
|
2171
|
-
text: summaries.map((cs, i) => {
|
|
2172
|
-
const budgetHint = topicBudgets?.get(cs.topic);
|
|
2173
|
-
const budgetLine = budgetHint ? `
|
|
2174
|
-
Budget: ~` + budgetHint + " tokens" : "";
|
|
2175
|
-
return "### Segment " + (i + 1) + ": " + cs.topic + `
|
|
2176
|
-
Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
|
|
2177
|
-
|
|
2178
|
-
` + cs.summary + `
|
|
2179
|
-
|
|
2180
|
-
Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
|
|
2181
|
-
Modified: ` + (cs.filesModified.join(", ") || "None") + `
|
|
2182
|
-
Read: ` + (cs.filesRead.join(", ") || "None");
|
|
2183
|
-
}).join(`
|
|
2184
|
-
---
|
|
2185
|
-
`)
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
function buildExtractionContext(extraction, forRange) {
|
|
2189
|
-
const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
|
|
2190
|
-
const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
|
|
2191
|
-
const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
|
|
2192
|
-
return [
|
|
2193
|
-
"## Deterministic Extraction (verified facts)",
|
|
2194
|
-
"Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
|
|
2195
|
-
"Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
|
|
2196
|
-
"Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 60)).join("; ") || "none"),
|
|
2197
|
-
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none"),
|
|
2198
|
-
"Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
|
|
2199
|
-
].join(`
|
|
2200
|
-
`);
|
|
2350
|
+
return decisions;
|
|
2201
2351
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2352
|
+
var CONSTRAINT_PATTERNS = [
|
|
2353
|
+
{ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
|
|
2354
|
+
{ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
|
|
2355
|
+
{ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
|
|
2356
|
+
{ re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
|
|
2357
|
+
{ re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
|
|
2358
|
+
{ re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
|
|
2359
|
+
];
|
|
2360
|
+
function mineConstraints(msgs) {
|
|
2361
|
+
const constraints = [];
|
|
2362
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2363
|
+
if (msgs[i]?.role !== "user")
|
|
2208
2364
|
continue;
|
|
2209
|
-
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
const block = part;
|
|
2217
|
-
if (block.type === "text" && typeof block.text === "string")
|
|
2218
|
-
mc += block.text.length;
|
|
2219
|
-
else if (block.type === "text" && typeof block.content === "string")
|
|
2220
|
-
mc += block.content.length;
|
|
2365
|
+
const txt = extractText(msgs[i].content);
|
|
2366
|
+
if (txt.length < 10 || txt.startsWith("/"))
|
|
2367
|
+
continue;
|
|
2368
|
+
for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
|
|
2369
|
+
if (re.test(txt)) {
|
|
2370
|
+
constraints.push({ index: i, text: txt.slice(0, TRUNC.CONSTRAINT_TEXT), category: cat, confidence: conf });
|
|
2371
|
+
break;
|
|
2221
2372
|
}
|
|
2222
2373
|
}
|
|
2223
|
-
totalChars += mc;
|
|
2224
|
-
if (m.role === "toolResult")
|
|
2225
|
-
toolChars += mc;
|
|
2226
2374
|
}
|
|
2227
|
-
return
|
|
2375
|
+
return constraints;
|
|
2228
2376
|
}
|
|
2229
|
-
function
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2377
|
+
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
2378
|
+
const topics = [];
|
|
2379
|
+
let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
|
|
2380
|
+
let currentType = "exploration";
|
|
2381
|
+
let currentPrimaryFile = null;
|
|
2382
|
+
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
2383
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2384
|
+
const m = msgs[i];
|
|
2385
|
+
const txt = extractText(m.content);
|
|
2386
|
+
tokenAcc += estimateTokens(txt);
|
|
2387
|
+
let brk = false;
|
|
2388
|
+
if (m.role === "assistant") {
|
|
2389
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
2390
|
+
for (const b of blocks) {
|
|
2391
|
+
for (const tool of flattenToolCallBlock(b)) {
|
|
2392
|
+
const fp = extractToolPath(tool.arguments);
|
|
2393
|
+
if (!fp)
|
|
2394
|
+
continue;
|
|
2395
|
+
const fn = path3.basename(fp);
|
|
2396
|
+
if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
|
|
2397
|
+
brk = true;
|
|
2398
|
+
lastFile = fn;
|
|
2399
|
+
currentPrimaryFile = fp;
|
|
2400
|
+
const toolCls = classifyTool(tool.arguments);
|
|
2401
|
+
if (toolCls === "mutates") {
|
|
2402
|
+
if (currentType !== "implementation")
|
|
2403
|
+
currentType = "implementation";
|
|
2404
|
+
} else if (toolCls === "accesses") {
|
|
2405
|
+
if (currentType === "exploration")
|
|
2406
|
+
currentType = "review";
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (m.role === "toolResult" && m.isError) {
|
|
2412
|
+
errAcc++;
|
|
2413
|
+
if (currentType !== "implementation")
|
|
2414
|
+
currentType = "debugging";
|
|
2415
|
+
}
|
|
2416
|
+
if (m.role === "toolResult" && !m.isError) {
|
|
2417
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
2418
|
+
if (tc && classifyTool(tc.arguments) === "executes" && /error|fail/i.test(txt)) {
|
|
2419
|
+
errAcc++;
|
|
2420
|
+
if (currentType !== "implementation")
|
|
2421
|
+
currentType = "debugging";
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
|
|
2425
|
+
brk = true;
|
|
2426
|
+
if (tokenAcc >= pc.maxChunkTokens)
|
|
2427
|
+
brk = true;
|
|
2428
|
+
if (brk && i > startIdx && topics.length < maxSegs - 1) {
|
|
2429
|
+
topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
2430
|
+
startIdx = i + 1;
|
|
2431
|
+
tokenAcc = 0;
|
|
2432
|
+
lastFile = null;
|
|
2433
|
+
errAcc = 0;
|
|
2434
|
+
currentType = "exploration";
|
|
2435
|
+
currentPrimaryFile = null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (startIdx < msgs.length) {
|
|
2439
|
+
topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
2440
|
+
}
|
|
2441
|
+
return topics;
|
|
2237
2442
|
}
|
|
2238
|
-
function
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
if (hasModifications)
|
|
2253
|
-
return "implementation";
|
|
2254
|
-
return "implementation";
|
|
2255
|
-
}
|
|
2256
|
-
function buildExplorationContext(report) {
|
|
2257
|
-
if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
|
|
2258
|
-
return "";
|
|
2259
|
-
return [
|
|
2260
|
-
"## Exploration Report",
|
|
2261
|
-
"Main goal: " + report.mainGoal,
|
|
2262
|
-
"Session type: " + report.sessionType,
|
|
2263
|
-
report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
|
|
2264
|
-
report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
|
|
2265
|
-
report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
|
|
2266
|
-
report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
|
|
2267
|
-
report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
|
|
2268
|
-
].filter(Boolean).join(`
|
|
2269
|
-
`);
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// src/index.ts
|
|
2273
|
-
init_tokens();
|
|
2274
|
-
init_cache();
|
|
2275
|
-
|
|
2276
|
-
// src/app/run-smart-compact.ts
|
|
2277
|
-
init_overlays();
|
|
2278
|
-
init_logger();
|
|
2279
|
-
|
|
2280
|
-
// src/app/run-context.ts
|
|
2281
|
-
function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
|
|
2282
|
-
rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
|
|
2283
|
-
rc.phaseStart = endMs;
|
|
2284
|
-
}
|
|
2285
|
-
function markPhase(rc, phase) {
|
|
2286
|
-
markMeasuredPhase(rc, phase, rc.phaseStart);
|
|
2443
|
+
function buildTimeline(msgs, errors) {
|
|
2444
|
+
const timeline = [];
|
|
2445
|
+
const errorIndices = new Set(errors.map((e) => e.index));
|
|
2446
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
2447
|
+
const m = msgs[i];
|
|
2448
|
+
if (m.role === "user") {
|
|
2449
|
+
const txt = extractText(m.content);
|
|
2450
|
+
if (!txt.startsWith("/"))
|
|
2451
|
+
timeline.push({ index: i, event: "user_request", summary: txt.slice(0, TRUNC.TIMELINE_EVENT) });
|
|
2452
|
+
}
|
|
2453
|
+
if (errorIndices.has(i))
|
|
2454
|
+
timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, TRUNC.TIMELINE_ERROR) ?? "error" });
|
|
2455
|
+
}
|
|
2456
|
+
return timeline.length > 30 ? [...timeline.filter((t) => t.event === "user_request").slice(0, TRUNC.TIMELINE_DISPLAY), ...timeline.filter((t) => t.event === "error")] : timeline;
|
|
2287
2457
|
}
|
|
2288
|
-
function
|
|
2289
|
-
|
|
2458
|
+
function extractMainGoal(msgs) {
|
|
2459
|
+
for (const m of msgs) {
|
|
2460
|
+
if (m?.role !== "user")
|
|
2461
|
+
continue;
|
|
2462
|
+
const txt = extractText(m.content).trim();
|
|
2463
|
+
if (txt && !txt.startsWith("/"))
|
|
2464
|
+
return txt.slice(0, TRUNC.MESSAGE);
|
|
2465
|
+
}
|
|
2466
|
+
return null;
|
|
2290
2467
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2468
|
+
function extractOpenLoops(msgs, extraction) {
|
|
2469
|
+
const loops = [];
|
|
2470
|
+
let loopId = 0;
|
|
2471
|
+
const fileNeedles = extraction.modifiedFiles.map((f) => ({
|
|
2472
|
+
path: f.path,
|
|
2473
|
+
needles: buildPathNeedles(f.path)
|
|
2474
|
+
}));
|
|
2475
|
+
for (const err of extraction.errors.filter((e) => !e.resolved)) {
|
|
2476
|
+
const errLower = err.message.toLowerCase();
|
|
2477
|
+
const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path4 }) => path4);
|
|
2478
|
+
loops.push({
|
|
2479
|
+
id: ID_PREFIX.OPEN_LOOP + ++loopId,
|
|
2480
|
+
type: "bugfix",
|
|
2481
|
+
priority: err.retryAttempted ? "high" : "normal",
|
|
2482
|
+
status: "open",
|
|
2483
|
+
summary: err.message.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
|
|
2484
|
+
files: errFiles,
|
|
2485
|
+
sourceIndex: err.index
|
|
2486
|
+
});
|
|
2309
2487
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2488
|
+
const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|yapalim|yapal\u0131m|yapmamiz|yapmam\u0131z|gerekiyor|eklenecek|d\u00FCzeltilecek|duzeltilecek|bitmedi|kaldi|kald\u0131)/iu;
|
|
2489
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
2490
|
+
const msg = msgs[idx];
|
|
2491
|
+
if (msg.role !== "user")
|
|
2492
|
+
continue;
|
|
2493
|
+
const txt = extractText(msg.content);
|
|
2494
|
+
if (txt.length < 10 || txt.startsWith("/"))
|
|
2495
|
+
continue;
|
|
2496
|
+
if (FOLLOWUP_RE.test(txt)) {
|
|
2497
|
+
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
2498
|
+
if (!isDup) {
|
|
2499
|
+
loops.push({
|
|
2500
|
+
id: ID_PREFIX.OPEN_LOOP + ++loopId,
|
|
2501
|
+
type: "follow-up",
|
|
2502
|
+
priority: "normal",
|
|
2503
|
+
status: "open",
|
|
2504
|
+
summary: txt.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
|
|
2505
|
+
files: [],
|
|
2506
|
+
sourceIndex: idx
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2316
2510
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
|
|
2354
|
-
function isStringArray(v) {
|
|
2355
|
-
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
2356
|
-
}
|
|
2357
|
-
function isValidSmartCompactDetails(d) {
|
|
2358
|
-
if (!d || typeof d !== "object")
|
|
2359
|
-
return false;
|
|
2360
|
-
const r = d;
|
|
2361
|
-
if (!isStringArray(r.modifiedFiles))
|
|
2362
|
-
return false;
|
|
2363
|
-
if (!isStringArray(r.readFiles))
|
|
2364
|
-
return false;
|
|
2365
|
-
if (!isStringArray(r.topics))
|
|
2366
|
-
return false;
|
|
2367
|
-
if (typeof r.method !== "string" || !KNOWN_METHODS.has(r.method))
|
|
2368
|
-
return false;
|
|
2369
|
-
if (typeof r.profile !== "string" || !KNOWN_PROFILES.has(r.profile))
|
|
2370
|
-
return false;
|
|
2371
|
-
if (typeof r.qualityScore !== "number" || !Number.isFinite(r.qualityScore))
|
|
2372
|
-
return false;
|
|
2373
|
-
if (typeof r.totalMessages !== "number" || !Number.isFinite(r.totalMessages))
|
|
2374
|
-
return false;
|
|
2375
|
-
if (r.gaps !== undefined && !isStringArray(r.gaps))
|
|
2376
|
-
return false;
|
|
2377
|
-
if (r.verified !== undefined && typeof r.verified !== "boolean")
|
|
2378
|
-
return false;
|
|
2379
|
-
if (r.backupPath !== undefined && r.backupPath !== null && typeof r.backupPath !== "string")
|
|
2380
|
-
return false;
|
|
2381
|
-
return true;
|
|
2511
|
+
const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
|
|
2512
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
2513
|
+
const msg = msgs[idx];
|
|
2514
|
+
if (msg.role !== "user")
|
|
2515
|
+
continue;
|
|
2516
|
+
const txt = extractText(msg.content);
|
|
2517
|
+
if (BLOCKED_RE.test(txt)) {
|
|
2518
|
+
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
2519
|
+
if (!isDup) {
|
|
2520
|
+
loops.push({
|
|
2521
|
+
id: ID_PREFIX.OPEN_LOOP + ++loopId,
|
|
2522
|
+
type: "blocked",
|
|
2523
|
+
priority: "high",
|
|
2524
|
+
status: "open",
|
|
2525
|
+
summary: txt.slice(0, TRUNC.OPEN_LOOP_SUMMARY),
|
|
2526
|
+
files: [],
|
|
2527
|
+
sourceIndex: idx
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
|
|
2533
|
+
const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
|
|
2534
|
+
if (!exists) {
|
|
2535
|
+
loops.push({
|
|
2536
|
+
id: ID_PREFIX.OPEN_LOOP + ++loopId,
|
|
2537
|
+
type: "retry",
|
|
2538
|
+
priority: "high",
|
|
2539
|
+
status: "open",
|
|
2540
|
+
summary: "Retried but unresolved: " + err.message.slice(0, TRUNC.SNIPPET),
|
|
2541
|
+
files: [],
|
|
2542
|
+
sourceIndex: err.index
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return loops;
|
|
2382
2547
|
}
|
|
2383
|
-
function
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
modifiedFiles:
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
tokensBefore: typeof r.tokensBefore === "number" ? r.tokensBefore : 0
|
|
2548
|
+
function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
2549
|
+
const tcIdx = precomputedTcIdx ?? buildToolCallIndex(msgs);
|
|
2550
|
+
const { modified, read, deleted } = trackFileOps(msgs, tcIdx);
|
|
2551
|
+
const errors = catalogErrors(msgs, tcIdx);
|
|
2552
|
+
const decisions = extractDecisions(msgs, tcIdx);
|
|
2553
|
+
const constraints = mineConstraints(msgs);
|
|
2554
|
+
const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
|
|
2555
|
+
const timeline = buildTimeline(msgs, errors);
|
|
2556
|
+
const mediaAttachments = extractMediaAttachments(msgs);
|
|
2557
|
+
const mainGoal = extractMainGoal(msgs);
|
|
2558
|
+
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
2559
|
+
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
2560
|
+
return {
|
|
2561
|
+
modifiedFiles: modified,
|
|
2562
|
+
readFiles: read,
|
|
2563
|
+
deletedFiles: deleted,
|
|
2564
|
+
errors,
|
|
2565
|
+
decisions,
|
|
2566
|
+
constraints,
|
|
2567
|
+
topics,
|
|
2568
|
+
timeline,
|
|
2569
|
+
mediaAttachments,
|
|
2570
|
+
mainGoal,
|
|
2571
|
+
lastUserMessages,
|
|
2572
|
+
lastErrors,
|
|
2573
|
+
messageCount: msgs.length
|
|
2410
2574
|
};
|
|
2411
|
-
return isValidSmartCompactDetails(repaired) ? repaired : null;
|
|
2412
2575
|
}
|
|
2413
2576
|
|
|
2414
|
-
// src/utils/
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
"
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
"utils.ts",
|
|
2423
|
-
"main.ts",
|
|
2424
|
-
"main.js",
|
|
2425
|
-
"mod.rs",
|
|
2426
|
-
"lib.rs",
|
|
2427
|
-
"__init__.py"
|
|
2428
|
-
]);
|
|
2429
|
-
var MIN_BARE_BASENAME_LEN = 5;
|
|
2430
|
-
function buildPathNeedles(filePath) {
|
|
2431
|
-
const parts = filePath.toLowerCase().split("/").filter(Boolean);
|
|
2432
|
-
if (parts.length === 0)
|
|
2433
|
-
return [];
|
|
2434
|
-
const needles = [];
|
|
2435
|
-
const basename = parts[parts.length - 1];
|
|
2436
|
-
if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
|
|
2437
|
-
needles.push(basename);
|
|
2577
|
+
// src/utils/helpers.ts
|
|
2578
|
+
init_constants();
|
|
2579
|
+
var VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
2580
|
+
var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
|
|
2581
|
+
function validateSmartCompactConfig(sc) {
|
|
2582
|
+
if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
|
|
2583
|
+
warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
|
|
2584
|
+
delete sc.profile;
|
|
2438
2585
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2586
|
+
if ("autoTrigger" in sc && typeof sc.autoTrigger !== "boolean") {
|
|
2587
|
+
warn("smart-compact config: autoTrigger must be boolean, got " + typeof sc.autoTrigger);
|
|
2588
|
+
delete sc.autoTrigger;
|
|
2441
2589
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2590
|
+
if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
|
|
2591
|
+
warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
|
|
2592
|
+
delete sc.backupEnabled;
|
|
2593
|
+
}
|
|
2594
|
+
if ("summaryModel" in sc && sc.summaryModel !== null && typeof sc.summaryModel !== "string") {
|
|
2595
|
+
warn("smart-compact config: summaryModel must be string|null, got " + typeof sc.summaryModel);
|
|
2596
|
+
delete sc.summaryModel;
|
|
2597
|
+
}
|
|
2598
|
+
if ("segmentationModel" in sc && sc.segmentationModel !== null && typeof sc.segmentationModel !== "string") {
|
|
2599
|
+
warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
|
|
2600
|
+
delete sc.segmentationModel;
|
|
2601
|
+
}
|
|
2602
|
+
if ("profiles" in sc) {
|
|
2603
|
+
if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
|
|
2604
|
+
warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
2605
|
+
delete sc.profiles;
|
|
2606
|
+
} else {
|
|
2607
|
+
const profiles = sc.profiles;
|
|
2608
|
+
for (const [profileName, value] of Object.entries(profiles)) {
|
|
2609
|
+
if (!VALID_PROFILES.includes(profileName)) {
|
|
2610
|
+
warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
|
|
2611
|
+
delete profiles[profileName];
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2615
|
+
warn("smart-compact config: profile '" + profileName + "' must be an object.");
|
|
2616
|
+
delete profiles[profileName];
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const profileCfg = value;
|
|
2620
|
+
for (const [key, raw] of Object.entries(profileCfg)) {
|
|
2621
|
+
if (!PROFILE_NUMERIC_KEYS.includes(key)) {
|
|
2622
|
+
warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
|
|
2623
|
+
delete profileCfg[key];
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
|
|
2627
|
+
warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
|
|
2628
|
+
delete profileCfg[key];
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if ("autoTriggerTimeoutMs" in sc) {
|
|
2635
|
+
const v = sc.autoTriggerTimeoutMs;
|
|
2636
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 1000 || v > 300000) {
|
|
2637
|
+
warn("smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + v + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms.");
|
|
2638
|
+
delete sc.autoTriggerTimeoutMs;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
if ("minContextPercent" in sc) {
|
|
2642
|
+
const v = sc.minContextPercent;
|
|
2643
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
|
|
2644
|
+
warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
|
|
2645
|
+
delete sc.minContextPercent;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if ("backupDir" in sc && sc.backupDir !== undefined && typeof sc.backupDir !== "string") {
|
|
2649
|
+
warn("smart-compact config: backupDir must be a string, got " + typeof sc.backupDir + ". Using default.");
|
|
2650
|
+
delete sc.backupDir;
|
|
2651
|
+
}
|
|
2652
|
+
if ("pinPaths" in sc && sc.pinPaths !== undefined) {
|
|
2653
|
+
if (!Array.isArray(sc.pinPaths) || !sc.pinPaths.every((x) => typeof x === "string")) {
|
|
2654
|
+
warn("smart-compact config: pinPaths must be a string[], ignoring.");
|
|
2655
|
+
delete sc.pinPaths;
|
|
2656
|
+
}
|
|
2468
2657
|
}
|
|
2469
|
-
return [{ name: b.name, id: b.id, arguments: b.arguments }];
|
|
2470
|
-
}
|
|
2471
|
-
function extractText(content) {
|
|
2472
|
-
if (typeof content === "string")
|
|
2473
|
-
return content;
|
|
2474
|
-
if (!Array.isArray(content))
|
|
2475
|
-
return "";
|
|
2476
|
-
return content.map((b) => {
|
|
2477
|
-
if (typeof b === "string")
|
|
2478
|
-
return b;
|
|
2479
|
-
if (isTextBlock(b))
|
|
2480
|
-
return b.text;
|
|
2481
|
-
return "";
|
|
2482
|
-
}).join("");
|
|
2483
2658
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2659
|
+
var _cfg = null;
|
|
2660
|
+
var _cfgMtime = 0;
|
|
2661
|
+
var _cfgPath = null;
|
|
2662
|
+
function loadConfig() {
|
|
2663
|
+
try {
|
|
2664
|
+
const p = settingsFile();
|
|
2665
|
+
const stat = fs2.statSync(p);
|
|
2666
|
+
if (_cfg && _cfgPath === p && stat.mtimeMs === _cfgMtime)
|
|
2667
|
+
return _cfg;
|
|
2668
|
+
const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
2669
|
+
const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
|
|
2670
|
+
validateSmartCompactConfig(sc);
|
|
2671
|
+
const merged = { ...DEFAULT_CONFIG, ...sc };
|
|
2672
|
+
if (sc.profiles)
|
|
2673
|
+
merged.profiles = { ...PROFILES, ...sc.profiles };
|
|
2674
|
+
if (!merged.backupDir)
|
|
2675
|
+
merged.backupDir = defaultBackupDir();
|
|
2676
|
+
_cfg = merged;
|
|
2677
|
+
_cfgMtime = stat.mtimeMs;
|
|
2678
|
+
_cfgPath = p;
|
|
2679
|
+
return _cfg;
|
|
2680
|
+
} catch (e) {
|
|
2681
|
+
debug("loadConfig: settings.json not found or unreadable, using defaults", e);
|
|
2682
|
+
const fallback = { ...DEFAULT_CONFIG, backupDir: defaultBackupDir() };
|
|
2683
|
+
_cfg = fallback;
|
|
2684
|
+
_cfgPath = null;
|
|
2685
|
+
return fallback;
|
|
2686
|
+
}
|
|
2495
2687
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
const
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2688
|
+
var _pruneInFlight = new Set;
|
|
2689
|
+
function prunePass(dir) {
|
|
2690
|
+
try {
|
|
2691
|
+
const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => {
|
|
2692
|
+
const full = path4.join(dir, name);
|
|
2693
|
+
try {
|
|
2694
|
+
return { full, mtimeMs: fs2.statSync(full).mtimeMs };
|
|
2695
|
+
} catch {
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
}).filter((v) => v !== null);
|
|
2699
|
+
const now = Date.now();
|
|
2700
|
+
const overAge = entries.filter((e) => now - e.mtimeMs > BACKUP_MAX_AGE_MS);
|
|
2701
|
+
const sorted = entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2702
|
+
const overCount = sorted.slice(BACKUP_MAX_FILES);
|
|
2703
|
+
const toRemove = new Set([...overAge, ...overCount].map((e) => e.full));
|
|
2704
|
+
for (const full of toRemove) {
|
|
2705
|
+
try {
|
|
2706
|
+
fs2.unlinkSync(full);
|
|
2707
|
+
} catch (e) {
|
|
2708
|
+
debug("prunePass unlink failed", e);
|
|
2514
2709
|
}
|
|
2515
2710
|
}
|
|
2711
|
+
} catch (e) {
|
|
2712
|
+
debug("prunePass scan failed", e);
|
|
2516
2713
|
}
|
|
2517
|
-
return out;
|
|
2518
2714
|
}
|
|
2519
|
-
function
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2715
|
+
function schedulePruneBackups(dir) {
|
|
2716
|
+
if (_pruneInFlight.has(dir))
|
|
2717
|
+
return;
|
|
2718
|
+
_pruneInFlight.add(dir);
|
|
2719
|
+
queueMicrotask(() => {
|
|
2720
|
+
try {
|
|
2721
|
+
prunePass(dir);
|
|
2722
|
+
} finally {
|
|
2723
|
+
_pruneInFlight.delete(dir);
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
function backupConversation(convText, sessionId) {
|
|
2728
|
+
try {
|
|
2729
|
+
const cfg = loadConfig();
|
|
2730
|
+
if (!cfg.backupEnabled)
|
|
2731
|
+
return null;
|
|
2732
|
+
const dir = cfg.backupDir;
|
|
2733
|
+
ensureDir(dir);
|
|
2734
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2735
|
+
const hash = crypto2.createHash("sha256").update(convText).digest("hex").slice(0, TRUNC.CONV_HASH);
|
|
2736
|
+
const fp = path4.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
|
|
2737
|
+
atomicWriteFileSync(fp, `# Smart Compact Backup
|
|
2738
|
+
# Date: ` + new Date().toISOString() + `
|
|
2739
|
+
# Session: ` + sessionId + `
|
|
2740
|
+
|
|
2741
|
+
` + convText);
|
|
2742
|
+
schedulePruneBackups(dir);
|
|
2743
|
+
return fp;
|
|
2744
|
+
} catch (e) {
|
|
2745
|
+
warn("backupConversation failed", e);
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
function listBackups(limit = 20) {
|
|
2750
|
+
try {
|
|
2751
|
+
const dir = loadConfig().backupDir;
|
|
2752
|
+
if (!fs2.existsSync(dir))
|
|
2753
|
+
return [];
|
|
2754
|
+
const out = [];
|
|
2755
|
+
for (const name of fs2.readdirSync(dir)) {
|
|
2756
|
+
if (!name.endsWith(".md"))
|
|
2528
2757
|
continue;
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2758
|
+
const full = path4.join(dir, name);
|
|
2759
|
+
try {
|
|
2760
|
+
const stat = fs2.statSync(full);
|
|
2761
|
+
if (!stat.isFile())
|
|
2762
|
+
continue;
|
|
2763
|
+
const head = fs2.readFileSync(full, "utf-8").split(`
|
|
2764
|
+
`).slice(0, TRUNC.BACKUP_PREVIEW_LINES).join(`
|
|
2765
|
+
`);
|
|
2766
|
+
const date = head.match(/^# Date:\s*(.+)$/m)?.[1]?.trim();
|
|
2767
|
+
const session = head.match(/^# Session:\s*(.+)$/m)?.[1]?.trim();
|
|
2768
|
+
out.push({
|
|
2769
|
+
path: full,
|
|
2770
|
+
sessionId: session ?? name,
|
|
2771
|
+
date: date ?? stat.mtime.toISOString(),
|
|
2772
|
+
sizeBytes: stat.size
|
|
2773
|
+
});
|
|
2774
|
+
} catch {}
|
|
2540
2775
|
}
|
|
2776
|
+
out.sort((a, b) => a.date < b.date ? 1 : a.date > b.date ? -1 : 0);
|
|
2777
|
+
return out.slice(0, limit);
|
|
2778
|
+
} catch (e) {
|
|
2779
|
+
warn("listBackups failed", e);
|
|
2780
|
+
return [];
|
|
2541
2781
|
}
|
|
2542
|
-
return idx;
|
|
2543
2782
|
}
|
|
2544
|
-
function
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
continue;
|
|
2560
|
-
const tool = tc.name.toLowerCase();
|
|
2561
|
-
if (hasToolHint(tool, WRITE_TOOL_HINTS)) {
|
|
2562
|
-
const resultText = extractText(m.content);
|
|
2563
|
-
if (isTruncated(resultText)) {
|
|
2564
|
-
const existing = modMap.get(filePath);
|
|
2565
|
-
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
2566
|
-
} else if (!NO_OP_RE.test(resultText)) {
|
|
2567
|
-
const existing = modMap.get(filePath);
|
|
2568
|
-
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
2569
|
-
}
|
|
2570
|
-
} else if (hasToolHint(tool, DELETE_TOOL_HINTS)) {
|
|
2571
|
-
delSet.add(filePath);
|
|
2572
|
-
} else if (hasToolHint(tool, READ_TOOL_HINTS)) {
|
|
2573
|
-
readSet.add(filePath);
|
|
2574
|
-
}
|
|
2783
|
+
function readBackupContent(fp) {
|
|
2784
|
+
try {
|
|
2785
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
2786
|
+
const lines = raw.split(`
|
|
2787
|
+
`);
|
|
2788
|
+
let i = 0;
|
|
2789
|
+
while (i < lines.length && lines[i].startsWith("#"))
|
|
2790
|
+
i++;
|
|
2791
|
+
if (i < lines.length && lines[i].trim() === "")
|
|
2792
|
+
i++;
|
|
2793
|
+
return lines.slice(i).join(`
|
|
2794
|
+
`).trim() || null;
|
|
2795
|
+
} catch (e) {
|
|
2796
|
+
warn("readBackupContent failed", e);
|
|
2797
|
+
return null;
|
|
2575
2798
|
}
|
|
2799
|
+
}
|
|
2800
|
+
function buildRestoreMessage(content, source) {
|
|
2576
2801
|
return {
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2802
|
+
customType: "smart-compact-restore",
|
|
2803
|
+
content: `# Restored pre-compaction context (smart-compact backup)
|
|
2804
|
+
Source: ` + source + `
|
|
2805
|
+
|
|
2806
|
+
` + content,
|
|
2807
|
+
display: true,
|
|
2808
|
+
details: { source, restoredAt: Date.now() }
|
|
2580
2809
|
};
|
|
2581
2810
|
}
|
|
2582
|
-
function
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2811
|
+
function getPreviousCompactionContext(branch) {
|
|
2812
|
+
const compactions = branch.filter((e) => e.type === "compaction");
|
|
2813
|
+
if (!compactions.length)
|
|
2814
|
+
return "";
|
|
2815
|
+
const last = compactions[compactions.length - 1];
|
|
2816
|
+
const topics = last.details?.topics ?? [];
|
|
2817
|
+
if (!topics.length)
|
|
2818
|
+
return "";
|
|
2819
|
+
return `
|
|
2820
|
+
[IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
|
|
2821
|
+
}
|
|
2822
|
+
function findLastAnchorIndex(branchEntries) {
|
|
2823
|
+
for (let i = branchEntries.length - 1;i >= 0; i--) {
|
|
2824
|
+
const e = branchEntries[i];
|
|
2825
|
+
if (e?.type !== "message")
|
|
2588
2826
|
continue;
|
|
2589
|
-
const
|
|
2590
|
-
if (
|
|
2591
|
-
errors.push({ index: i, tool: tc?.name ?? "unknown", message: extractText(m.content).slice(0, 500), retryAttempted: false, resolved: false });
|
|
2827
|
+
const msg = e.message;
|
|
2828
|
+
if (msg?.role !== "toolResult")
|
|
2592
2829
|
continue;
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
const txt = extractText(m.content);
|
|
2596
|
-
const isLikelyError = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i.test(txt);
|
|
2597
|
-
if (isLikelyError && txt.length < 2000) {
|
|
2598
|
-
errors.push({ index: i, tool: "bash", message: txt.slice(0, 300), retryAttempted: false, resolved: false });
|
|
2599
|
-
}
|
|
2830
|
+
if (msg?.toolName === "context" && msg?.details?.anchor) {
|
|
2831
|
+
return i;
|
|
2600
2832
|
}
|
|
2601
2833
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
if (msgs[k]?.role === "toolResult" && msgs[k]?.toolCallId === tool.id && !msgs[k]?.isError) {
|
|
2613
|
-
err.resolved = true;
|
|
2614
|
-
break;
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
break;
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
if (err.retryAttempted)
|
|
2621
|
-
break;
|
|
2622
|
-
}
|
|
2623
|
-
if (err.retryAttempted)
|
|
2624
|
-
break;
|
|
2625
|
-
}
|
|
2834
|
+
return -1;
|
|
2835
|
+
}
|
|
2836
|
+
function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
|
|
2837
|
+
let msgCount = 0;
|
|
2838
|
+
for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
|
|
2839
|
+
const e = branchEntries[i];
|
|
2840
|
+
if (e?.type === "message") {
|
|
2841
|
+
if (msgCount >= msgs.length)
|
|
2842
|
+
return msgs.length - 1;
|
|
2843
|
+
msgCount++;
|
|
2626
2844
|
}
|
|
2627
2845
|
}
|
|
2628
|
-
return
|
|
2846
|
+
return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
|
|
2629
2847
|
}
|
|
2630
|
-
function
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
if (!question)
|
|
2639
|
-
continue;
|
|
2640
|
-
for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
|
|
2641
|
-
if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
|
|
2642
|
-
decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, 200), userResponse: extractText(msgs[i].content).slice(0, 300) });
|
|
2643
|
-
break;
|
|
2848
|
+
function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
|
|
2849
|
+
let adjusted = keepFromIndex;
|
|
2850
|
+
if (branchEntries && branchEntries.length > 0) {
|
|
2851
|
+
const lastAnchorBranchIdx = findLastAnchorIndex(branchEntries);
|
|
2852
|
+
if (lastAnchorBranchIdx >= 0) {
|
|
2853
|
+
const lastAnchorMsgIdx = branchIndexToMsgIndex(branchEntries, lastAnchorBranchIdx, msgs);
|
|
2854
|
+
if (adjusted > lastAnchorMsgIdx && lastAnchorMsgIdx >= 0) {
|
|
2855
|
+
adjusted = lastAnchorMsgIdx;
|
|
2644
2856
|
}
|
|
2645
2857
|
}
|
|
2646
2858
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
{ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: 0.85 },
|
|
2659
|
-
{ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: 0.8 },
|
|
2660
|
-
{ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: 0.6 },
|
|
2661
|
-
{ re: /\b(?:kritik|kritikal|\u00f6nemli|onemli|\u015fart|sart|zorunlu|\u015fart ko\u015ful|\u00f6nemli \u015fart|kesinlikle|kesinlikle \u015fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|b\u00f6yle olsun|b\u00f6yle yap\u0131n|\u015f\u00f6yle olsun|\u015f\u00f6yle yap\u0131n)\b/iu, cat: "requirement", conf: 0.8 },
|
|
2662
|
-
{ re: /\b(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))\b/iu, cat: "prohibition", conf: 0.8 },
|
|
2663
|
-
{ re: /\b(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)\b/iu, cat: "preference", conf: 0.6 }
|
|
2664
|
-
];
|
|
2665
|
-
function mineConstraints(msgs) {
|
|
2666
|
-
const constraints = [];
|
|
2667
|
-
for (let i = 0;i < msgs.length; i++) {
|
|
2668
|
-
if (msgs[i]?.role !== "user")
|
|
2669
|
-
continue;
|
|
2670
|
-
const txt = extractText(msgs[i].content);
|
|
2671
|
-
if (txt.length < 10 || txt.startsWith("/"))
|
|
2672
|
-
continue;
|
|
2673
|
-
for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
|
|
2674
|
-
if (re.test(txt)) {
|
|
2675
|
-
constraints.push({ index: i, text: txt.slice(0, 300), category: cat, confidence: conf });
|
|
2676
|
-
break;
|
|
2859
|
+
if (adjusted <= 0 || adjusted >= msgs.length)
|
|
2860
|
+
return adjusted;
|
|
2861
|
+
const touchedFiles = (msg) => {
|
|
2862
|
+
const m = msg;
|
|
2863
|
+
const blocks = Array.isArray(m?.content) ? m.content : [];
|
|
2864
|
+
const files = new Set;
|
|
2865
|
+
for (const b of blocks) {
|
|
2866
|
+
for (const tc of flattenToolCallBlock(b)) {
|
|
2867
|
+
const fp = extractToolPath(tc.arguments);
|
|
2868
|
+
if (fp)
|
|
2869
|
+
files.add(fp.split("/").pop() ?? fp);
|
|
2677
2870
|
}
|
|
2678
2871
|
}
|
|
2872
|
+
return files;
|
|
2873
|
+
};
|
|
2874
|
+
const lastFiles = touchedFiles(msgs[adjusted - 1].message);
|
|
2875
|
+
if (lastFiles.size > 0) {
|
|
2876
|
+
const keptFiles = touchedFiles(msgs[adjusted].message);
|
|
2877
|
+
if ([...lastFiles].some((f) => keptFiles.has(f)))
|
|
2878
|
+
return adjusted - 1;
|
|
2679
2879
|
}
|
|
2680
|
-
return
|
|
2880
|
+
return adjusted;
|
|
2681
2881
|
}
|
|
2682
|
-
function
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
if (m.role === "assistant") {
|
|
2696
|
-
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
2697
|
-
for (const b of blocks) {
|
|
2698
|
-
for (const tool of flattenToolCallBlock(b)) {
|
|
2699
|
-
const fp = tool.arguments?.path ?? tool.arguments?.file_path;
|
|
2700
|
-
if (!fp)
|
|
2701
|
-
continue;
|
|
2702
|
-
const fn = path6.basename(fp);
|
|
2703
|
-
if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
|
|
2704
|
-
brk = true;
|
|
2705
|
-
lastFile = fn;
|
|
2706
|
-
primaryFile = fp;
|
|
2707
|
-
currentPrimaryFile = fp;
|
|
2708
|
-
if (tool.name?.includes("write") || tool.name?.includes("edit")) {
|
|
2709
|
-
type = "implementation";
|
|
2710
|
-
if (currentType !== "implementation")
|
|
2711
|
-
currentType = "implementation";
|
|
2712
|
-
} else if (tool.name?.includes("read")) {
|
|
2713
|
-
type = "review";
|
|
2714
|
-
if (currentType === "exploration")
|
|
2715
|
-
currentType = "review";
|
|
2882
|
+
function collectToolCallIds(blocks, msgIndex, out) {
|
|
2883
|
+
for (const b of blocks) {
|
|
2884
|
+
const block = b;
|
|
2885
|
+
if (block?.type === "toolCall") {
|
|
2886
|
+
if (typeof block.id === "string") {
|
|
2887
|
+
out.set(block.id, msgIndex);
|
|
2888
|
+
}
|
|
2889
|
+
const args = block.arguments;
|
|
2890
|
+
if (block.name === "multi_tool_use.parallel" && args && Array.isArray(args.tool_uses)) {
|
|
2891
|
+
for (const nested of args.tool_uses) {
|
|
2892
|
+
const n = nested;
|
|
2893
|
+
if (typeof n.id === "string") {
|
|
2894
|
+
out.set(n.id, msgIndex);
|
|
2716
2895
|
}
|
|
2717
2896
|
}
|
|
2718
2897
|
}
|
|
2719
2898
|
}
|
|
2720
|
-
if (m.role === "toolResult" && m.isError) {
|
|
2721
|
-
errAcc++;
|
|
2722
|
-
type = "debugging";
|
|
2723
|
-
if (currentType !== "implementation")
|
|
2724
|
-
currentType = "debugging";
|
|
2725
|
-
}
|
|
2726
|
-
if (m.role === "toolResult" && !m.isError) {
|
|
2727
|
-
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
2728
|
-
if (tc?.name === "bash" && /error|fail/i.test(txt)) {
|
|
2729
|
-
errAcc++;
|
|
2730
|
-
type = "debugging";
|
|
2731
|
-
if (currentType !== "implementation")
|
|
2732
|
-
currentType = "debugging";
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
|
|
2736
|
-
brk = true;
|
|
2737
|
-
if (tokenAcc >= pc.maxChunkTokens)
|
|
2738
|
-
brk = true;
|
|
2739
|
-
if (brk && i > startIdx && topics.length < maxSegs - 1) {
|
|
2740
|
-
topics.push({ startIndex: startIdx, endIndex: i, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
2741
|
-
startIdx = i + 1;
|
|
2742
|
-
tokenAcc = 0;
|
|
2743
|
-
lastFile = null;
|
|
2744
|
-
errAcc = 0;
|
|
2745
|
-
currentType = "exploration";
|
|
2746
|
-
currentPrimaryFile = null;
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
if (startIdx < msgs.length) {
|
|
2750
|
-
topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
|
|
2751
2899
|
}
|
|
2752
|
-
return topics;
|
|
2753
2900
|
}
|
|
2754
|
-
function
|
|
2755
|
-
|
|
2756
|
-
|
|
2901
|
+
function guardToolCallBoundary(msgs, keepFrom) {
|
|
2902
|
+
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
2903
|
+
return keepFrom;
|
|
2904
|
+
const tcMap = new Map;
|
|
2757
2905
|
for (let i = 0;i < msgs.length; i++) {
|
|
2758
|
-
const m = msgs[i];
|
|
2759
|
-
if (m
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2906
|
+
const m = msgs[i].message;
|
|
2907
|
+
if (m?.role !== "assistant")
|
|
2908
|
+
continue;
|
|
2909
|
+
const blocks = Array.isArray(m?.content) ? m.content : [];
|
|
2910
|
+
collectToolCallIds(blocks, i, tcMap);
|
|
2911
|
+
}
|
|
2912
|
+
let adjusted = keepFrom;
|
|
2913
|
+
let changed = true;
|
|
2914
|
+
const MAX_ITER = msgs.length + 1;
|
|
2915
|
+
let iter = 0;
|
|
2916
|
+
while (changed) {
|
|
2917
|
+
if (++iter > MAX_ITER) {
|
|
2918
|
+
warn("guardToolCallBoundary hit MAX_ITER=" + MAX_ITER + " at adjusted=" + adjusted);
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
changed = false;
|
|
2922
|
+
for (let i = adjusted;i < msgs.length; i++) {
|
|
2923
|
+
const m = msgs[i].message;
|
|
2924
|
+
if (m?.role !== "toolResult")
|
|
2925
|
+
continue;
|
|
2926
|
+
const tcId = m?.toolCallId;
|
|
2927
|
+
if (!tcId)
|
|
2928
|
+
continue;
|
|
2929
|
+
const tcIdx = tcMap.get(tcId);
|
|
2930
|
+
if (tcIdx !== undefined && tcIdx < adjusted) {
|
|
2931
|
+
adjusted = tcIdx;
|
|
2932
|
+
changed = true;
|
|
2933
|
+
break;
|
|
2934
|
+
}
|
|
2763
2935
|
}
|
|
2764
|
-
if (errorIndices.has(i))
|
|
2765
|
-
timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, 100) ?? "error" });
|
|
2766
2936
|
}
|
|
2767
|
-
return
|
|
2937
|
+
return Math.max(0, Math.min(adjusted, msgs.length));
|
|
2768
2938
|
}
|
|
2769
|
-
function
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
return txt.slice(0, 300);
|
|
2776
|
-
}
|
|
2777
|
-
return null;
|
|
2939
|
+
function extractUserNote(args) {
|
|
2940
|
+
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
2941
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2942
|
+
const isOptionToken = (t) => t.startsWith("--") || SKIP.has(t.toLowerCase()) || /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t);
|
|
2943
|
+
const nonFlags = tokens.filter((t) => !isOptionToken(t));
|
|
2944
|
+
return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
|
|
2778
2945
|
}
|
|
2779
|
-
function
|
|
2780
|
-
const
|
|
2781
|
-
let
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const errLower = err.message.toLowerCase();
|
|
2788
|
-
const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path7 }) => path7);
|
|
2789
|
-
loops.push({
|
|
2790
|
-
id: "loop-" + ++loopId,
|
|
2791
|
-
type: "bugfix",
|
|
2792
|
-
priority: err.retryAttempted ? "high" : "normal",
|
|
2793
|
-
status: "open",
|
|
2794
|
-
summary: err.message.slice(0, 120),
|
|
2795
|
-
files: errFiles,
|
|
2796
|
-
sourceIndex: err.index
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|yapalim|yapal\u0131m|yapmamiz|yapmam\u0131z|gerekiyor|eklenecek|d\u00FCzeltilecek|duzeltilecek|bitmedi|kaldi|kald\u0131)/iu;
|
|
2800
|
-
for (let idx = 0;idx < msgs.length; idx++) {
|
|
2801
|
-
const msg = msgs[idx];
|
|
2802
|
-
if (msg.role !== "user")
|
|
2803
|
-
continue;
|
|
2804
|
-
const txt = extractText(msg.content);
|
|
2805
|
-
if (txt.length < 10 || txt.startsWith("/"))
|
|
2806
|
-
continue;
|
|
2807
|
-
if (FOLLOWUP_RE.test(txt)) {
|
|
2808
|
-
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
2809
|
-
if (!isDup) {
|
|
2810
|
-
loops.push({
|
|
2811
|
-
id: "loop-" + ++loopId,
|
|
2812
|
-
type: "follow-up",
|
|
2813
|
-
priority: "normal",
|
|
2814
|
-
status: "open",
|
|
2815
|
-
summary: txt.slice(0, 120),
|
|
2816
|
-
files: [],
|
|
2817
|
-
sourceIndex: idx
|
|
2818
|
-
});
|
|
2819
|
-
}
|
|
2946
|
+
function createBatches(chunks, maxTokens) {
|
|
2947
|
+
const batches = [];
|
|
2948
|
+
let batch = [], bt = 0;
|
|
2949
|
+
for (const ch of chunks) {
|
|
2950
|
+
if (batch.length && bt + ch.tokenEstimate > maxTokens) {
|
|
2951
|
+
batches.push(batch);
|
|
2952
|
+
batch = [];
|
|
2953
|
+
bt = 0;
|
|
2820
2954
|
}
|
|
2955
|
+
batch.push(ch);
|
|
2956
|
+
bt += ch.tokenEstimate;
|
|
2821
2957
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2958
|
+
if (batch.length)
|
|
2959
|
+
batches.push(batch);
|
|
2960
|
+
return batches;
|
|
2961
|
+
}
|
|
2962
|
+
function allocateTopicBudgets(summaries, totalBudget) {
|
|
2963
|
+
const n = summaries.length;
|
|
2964
|
+
if (n === 0)
|
|
2965
|
+
return new Map;
|
|
2966
|
+
const weights = summaries.map((s, i) => {
|
|
2967
|
+
let w = 1;
|
|
2968
|
+
if (s.priority === "critical")
|
|
2969
|
+
w *= 2;
|
|
2970
|
+
else if (s.priority === "high")
|
|
2971
|
+
w *= 1.5;
|
|
2972
|
+
else if (s.priority === "low")
|
|
2973
|
+
w *= 0.6;
|
|
2974
|
+
const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
|
|
2975
|
+
w *= 1 + errorKeywords * 0.2;
|
|
2976
|
+
const recency = (i + 1) / n;
|
|
2977
|
+
w *= 0.6 + recency * 0.4;
|
|
2978
|
+
if (s.keyDecisions.length > 0)
|
|
2979
|
+
w *= 1.3;
|
|
2980
|
+
return w;
|
|
2981
|
+
});
|
|
2982
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
2983
|
+
const baseTokensPerTopic = Math.floor(totalBudget / n);
|
|
2984
|
+
const budgetMap = new Map;
|
|
2985
|
+
for (let i = 0;i < summaries.length; i++) {
|
|
2986
|
+
const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
|
|
2987
|
+
budgetMap.set(summaries[i].topic, Math.max(200, allocated));
|
|
2988
|
+
}
|
|
2989
|
+
return budgetMap;
|
|
2990
|
+
}
|
|
2991
|
+
function preProcessSummaries(summaries, budgetTokens) {
|
|
2992
|
+
const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
|
|
2993
|
+
return {
|
|
2994
|
+
decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
|
|
2995
|
+
modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
|
|
2996
|
+
read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
|
|
2997
|
+
text: summaries.map((cs, i) => {
|
|
2998
|
+
const budgetHint = topicBudgets?.get(cs.topic);
|
|
2999
|
+
const budgetLine = budgetHint ? `
|
|
3000
|
+
Budget: ~` + budgetHint + " tokens" : "";
|
|
3001
|
+
return "### Segment " + (i + 1) + ": " + cs.topic + `
|
|
3002
|
+
Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
|
|
3003
|
+
|
|
3004
|
+
` + cs.summary + `
|
|
3005
|
+
|
|
3006
|
+
Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
|
|
3007
|
+
Modified: ` + (cs.filesModified.join(", ") || "None") + `
|
|
3008
|
+
Read: ` + (cs.filesRead.join(", ") || "None");
|
|
3009
|
+
}).join(`
|
|
3010
|
+
---
|
|
3011
|
+
`)
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function buildExtractionContext(extraction, forRange) {
|
|
3015
|
+
const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
|
|
3016
|
+
const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
|
|
3017
|
+
const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
|
|
3018
|
+
return [
|
|
3019
|
+
"## Deterministic Extraction (verified facts)",
|
|
3020
|
+
"Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
|
|
3021
|
+
"Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, TRUNC.SNIPPET) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
|
|
3022
|
+
"Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, TRUNC.DECISION_DETAIL)).join("; ") || "none"),
|
|
3023
|
+
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, TRUNC.DECISION_DETAIL)).join("; ") || "none"),
|
|
3024
|
+
"Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
|
|
3025
|
+
].join(`
|
|
3026
|
+
`);
|
|
3027
|
+
}
|
|
3028
|
+
function computeToolCharPercentage(branchEntries) {
|
|
3029
|
+
let totalChars = 0;
|
|
3030
|
+
let toolChars = 0;
|
|
3031
|
+
for (const raw of branchEntries) {
|
|
3032
|
+
const m = raw?.message;
|
|
3033
|
+
if (!m)
|
|
2826
3034
|
continue;
|
|
2827
|
-
|
|
2828
|
-
if (
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
files: [],
|
|
2838
|
-
sourceIndex: idx
|
|
2839
|
-
});
|
|
3035
|
+
let mc = 0;
|
|
3036
|
+
if (typeof m.content === "string") {
|
|
3037
|
+
mc = m.content.length;
|
|
3038
|
+
} else if (Array.isArray(m.content)) {
|
|
3039
|
+
for (const part of m.content) {
|
|
3040
|
+
if (!part || typeof part !== "object")
|
|
3041
|
+
continue;
|
|
3042
|
+
const block = part;
|
|
3043
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
3044
|
+
mc += block.text.length;
|
|
2840
3045
|
}
|
|
2841
3046
|
}
|
|
3047
|
+
totalChars += mc;
|
|
3048
|
+
if (m.role === "toolResult")
|
|
3049
|
+
toolChars += mc;
|
|
2842
3050
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3051
|
+
return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
|
|
3052
|
+
}
|
|
3053
|
+
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
|
|
3054
|
+
if (totalTokens < minThreshold)
|
|
3055
|
+
return "none";
|
|
3056
|
+
if (contextPercent < minContextPercent)
|
|
3057
|
+
return "none";
|
|
3058
|
+
if (contextPercent < 80)
|
|
3059
|
+
return "light";
|
|
3060
|
+
return "full";
|
|
3061
|
+
}
|
|
3062
|
+
function inferSessionType(extraction, report) {
|
|
3063
|
+
if (report?.sessionType)
|
|
3064
|
+
return report.sessionType;
|
|
3065
|
+
const hasModifications = extraction.modifiedFiles.length > 0;
|
|
3066
|
+
const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
|
|
3067
|
+
const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
|
|
3068
|
+
const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
|
|
3069
|
+
const hasDecisions = extraction.decisions.length > 0;
|
|
3070
|
+
if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
|
|
3071
|
+
return "debugging";
|
|
3072
|
+
if (hasReadsOnly && !hasDecisions)
|
|
3073
|
+
return "review";
|
|
3074
|
+
if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
|
|
3075
|
+
return "discussion";
|
|
3076
|
+
if (hasModifications)
|
|
3077
|
+
return "implementation";
|
|
3078
|
+
return "implementation";
|
|
3079
|
+
}
|
|
3080
|
+
function buildExplorationContext(report) {
|
|
3081
|
+
if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
|
|
3082
|
+
return "";
|
|
3083
|
+
return [
|
|
3084
|
+
"## Exploration Report",
|
|
3085
|
+
"Main goal: " + report.mainGoal,
|
|
3086
|
+
"Session type: " + report.sessionType,
|
|
3087
|
+
report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
|
|
3088
|
+
report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
|
|
3089
|
+
report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
|
|
3090
|
+
report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
|
|
3091
|
+
report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
|
|
3092
|
+
].filter(Boolean).join(`
|
|
3093
|
+
`);
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// src/index.ts
|
|
3097
|
+
init_tokens();
|
|
3098
|
+
init_cache();
|
|
3099
|
+
|
|
3100
|
+
// src/app/run-smart-compact.ts
|
|
3101
|
+
init_overlays();
|
|
3102
|
+
init_logger();
|
|
3103
|
+
|
|
3104
|
+
// src/app/run-context.ts
|
|
3105
|
+
function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
|
|
3106
|
+
rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
|
|
3107
|
+
rc.phaseStart = endMs;
|
|
3108
|
+
}
|
|
3109
|
+
function markPhase(rc, phase) {
|
|
3110
|
+
markMeasuredPhase(rc, phase, rc.phaseStart);
|
|
3111
|
+
}
|
|
3112
|
+
function advance(rc, _stage) {
|
|
3113
|
+
return rc;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// src/app/run-smart-compact.ts
|
|
3117
|
+
init_services();
|
|
3118
|
+
|
|
3119
|
+
// src/app/steps/prepare.ts
|
|
3120
|
+
init_constants();
|
|
3121
|
+
init_tokens();
|
|
3122
|
+
init_logger();
|
|
3123
|
+
async function prepareRun(rc) {
|
|
3124
|
+
const config = loadConfig();
|
|
3125
|
+
const profileCfg = { ...PROFILES[rc.profile], ...config.profiles?.[rc.profile] ?? {} };
|
|
3126
|
+
const providerCaps = getProviderCaps(rc.summaryModel.provider);
|
|
3127
|
+
const auth = await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.summaryModel);
|
|
3128
|
+
const segAuth = rc.segModel !== rc.summaryModel ? await rc.ctx.modelRegistry.getApiKeyAndHeaders(rc.segModel) : auth;
|
|
3129
|
+
if (!auth.ok || !auth.apiKey || (!segAuth.ok || !segAuth.apiKey)) {
|
|
3130
|
+
if (!rc.flags.autoTriggered)
|
|
3131
|
+
rc.ctx.ui.notify("Auth failed", "error");
|
|
3132
|
+
return null;
|
|
2856
3133
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
const
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
return
|
|
2872
|
-
modifiedFiles: modified,
|
|
2873
|
-
readFiles: read,
|
|
2874
|
-
deletedFiles: deleted,
|
|
2875
|
-
errors,
|
|
2876
|
-
decisions,
|
|
2877
|
-
constraints,
|
|
2878
|
-
topics,
|
|
2879
|
-
timeline,
|
|
2880
|
-
mediaAttachments,
|
|
2881
|
-
mainGoal,
|
|
2882
|
-
lastUserMessages,
|
|
2883
|
-
lastErrors,
|
|
2884
|
-
messageCount: msgs.length
|
|
2885
|
-
};
|
|
3134
|
+
if (rc.timeoutMs > 0) {
|
|
3135
|
+
rc.cancellation.timeoutId = setTimeout(() => {
|
|
3136
|
+
rc.cancellation.timedOut = true;
|
|
3137
|
+
rc.cancellation.controller.abort();
|
|
3138
|
+
rc.notify("Smart compact auto-trigger exceeded " + rc.timeoutMs + "ms; Pi will use native compact for this run", "warning");
|
|
3139
|
+
}, rc.timeoutMs);
|
|
3140
|
+
}
|
|
3141
|
+
debug("prepareRun: profile=" + rc.profile + " model=" + rc.modelLabel);
|
|
3142
|
+
const out = rc;
|
|
3143
|
+
out.config = config;
|
|
3144
|
+
out.profileCfg = profileCfg;
|
|
3145
|
+
out.providerCaps = providerCaps;
|
|
3146
|
+
out.summaryAuth = { apiKey: auth.apiKey, headers: auth.headers };
|
|
3147
|
+
out.segAuth = { apiKey: segAuth.apiKey, headers: segAuth.headers };
|
|
3148
|
+
return advance(out, "_prepared");
|
|
2886
3149
|
}
|
|
2887
3150
|
|
|
3151
|
+
// src/app/steps/window.ts
|
|
3152
|
+
init_tokens();
|
|
3153
|
+
|
|
2888
3154
|
// src/infra/session-identity.ts
|
|
2889
3155
|
import { randomUUID } from "crypto";
|
|
2890
3156
|
var UNRESOLVED_PREFIX = "unresolved:";
|
|
@@ -2947,6 +3213,14 @@ function resolveCompactionWindow(rc) {
|
|
|
2947
3213
|
// src/app/steps/recover.ts
|
|
2948
3214
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2949
3215
|
|
|
3216
|
+
// src/infra/ai-messages.ts
|
|
3217
|
+
function asBranchMessage(message) {
|
|
3218
|
+
return message;
|
|
3219
|
+
}
|
|
3220
|
+
function asSerializableMessages(msgs) {
|
|
3221
|
+
return msgs;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
2950
3224
|
// src/utils/session-log.ts
|
|
2951
3225
|
import * as fs4 from "fs";
|
|
2952
3226
|
import * as path7 from "path";
|
|
@@ -3059,17 +3333,19 @@ function findSessionLogFile(sessionId) {
|
|
|
3059
3333
|
}
|
|
3060
3334
|
return remember(null);
|
|
3061
3335
|
}
|
|
3062
|
-
function normalizeLogMessage(msg) {
|
|
3336
|
+
function normalizeLogMessage(msg, entryTimestamp) {
|
|
3063
3337
|
if (!msg || !msg.role)
|
|
3064
3338
|
return null;
|
|
3065
3339
|
const role = msg.role;
|
|
3066
3340
|
if (role === "user" || role === "assistant" || role === "toolResult") {
|
|
3341
|
+
const ts = entryTimestamp ? Date.parse(entryTimestamp) : NaN;
|
|
3067
3342
|
return {
|
|
3068
3343
|
role,
|
|
3069
3344
|
content: msg.content,
|
|
3070
3345
|
isError: msg.isError,
|
|
3071
3346
|
toolCallId: msg.toolCallId,
|
|
3072
|
-
|
|
3347
|
+
toolName: msg.toolName,
|
|
3348
|
+
timestamp: Number.isFinite(ts) ? ts : undefined
|
|
3073
3349
|
};
|
|
3074
3350
|
}
|
|
3075
3351
|
return null;
|
|
@@ -3100,7 +3376,7 @@ function readOriginalMessageMap(sessionId) {
|
|
|
3100
3376
|
continue;
|
|
3101
3377
|
}
|
|
3102
3378
|
if (entry.type === "message" && entry.id && entry.message) {
|
|
3103
|
-
const normalized = normalizeLogMessage(entry.message);
|
|
3379
|
+
const normalized = normalizeLogMessage(entry.message, entry.timestamp);
|
|
3104
3380
|
if (normalized)
|
|
3105
3381
|
map.set(entry.id, normalized);
|
|
3106
3382
|
}
|
|
@@ -3148,7 +3424,7 @@ function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
|
3148
3424
|
|
|
3149
3425
|
// src/app/steps/recover.ts
|
|
3150
3426
|
function recoverSessionLog(rc) {
|
|
3151
|
-
let llmMessages = convertToLlm(rc.toCompact.map((e) => e.message));
|
|
3427
|
+
let llmMessages = convertToLlm(rc.toCompact.map((e) => asBranchMessage(e.message)));
|
|
3152
3428
|
if (hasTruncatedMessages(llmMessages)) {
|
|
3153
3429
|
const fromLog = resolveCompactionMessages(rc.sessionId, rc.toCompact);
|
|
3154
3430
|
if (fromLog) {
|
|
@@ -3203,9 +3479,9 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3203
3479
|
if (msgs[i2].role !== "toolResult")
|
|
3204
3480
|
continue;
|
|
3205
3481
|
const tc = tcIdx.get(msgs[i2].toolCallId ?? "");
|
|
3206
|
-
if (!tc || tc.
|
|
3482
|
+
if (!tc || classifyTool(tc.arguments) !== "accesses")
|
|
3207
3483
|
continue;
|
|
3208
|
-
const fp = tc
|
|
3484
|
+
const fp = extractToolPath(tc.arguments);
|
|
3209
3485
|
if (!fp)
|
|
3210
3486
|
continue;
|
|
3211
3487
|
const arr = readIndices.get(fp) ?? [];
|
|
@@ -3284,7 +3560,8 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3284
3560
|
originalTokens += estimateTokens(text);
|
|
3285
3561
|
if (!keep.has(idx))
|
|
3286
3562
|
continue;
|
|
3287
|
-
|
|
3563
|
+
const protectFromTruncation = m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && text.length < ERROR_SCAN_MAX_LEN && LIKELY_ERROR_RE.test(text);
|
|
3564
|
+
if (m.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && !protectFromTruncation) {
|
|
3288
3565
|
const head = text.slice(0, half);
|
|
3289
3566
|
const tail = text.slice(-half);
|
|
3290
3567
|
const truncated = head + `
|
|
@@ -3319,7 +3596,7 @@ init_logger();
|
|
|
3319
3596
|
init_paths();
|
|
3320
3597
|
init_fs();
|
|
3321
3598
|
import path8 from "path";
|
|
3322
|
-
import
|
|
3599
|
+
import crypto5 from "crypto";
|
|
3323
3600
|
|
|
3324
3601
|
// src/infra/git.ts
|
|
3325
3602
|
init_logger();
|
|
@@ -3343,6 +3620,7 @@ function findGitRoot(cwd) {
|
|
|
3343
3620
|
}
|
|
3344
3621
|
|
|
3345
3622
|
// src/utils/fingerprint.ts
|
|
3623
|
+
init_constants();
|
|
3346
3624
|
var LANG_MAP = {
|
|
3347
3625
|
".ts": "typescript",
|
|
3348
3626
|
".tsx": "typescript",
|
|
@@ -3382,7 +3660,7 @@ function isProjectPath(filePath) {
|
|
|
3382
3660
|
return !NOISE_PATH_RE.test(filePath);
|
|
3383
3661
|
}
|
|
3384
3662
|
function hashProjectId(seed) {
|
|
3385
|
-
return
|
|
3663
|
+
return ID_PREFIX.PROJECT + crypto5.createHash("sha256").update(seed).digest("hex").slice(0, TRUNC.PROJ_ID_HASH);
|
|
3386
3664
|
}
|
|
3387
3665
|
function findGitRoot2(cwd) {
|
|
3388
3666
|
return findGitRoot(cwd);
|
|
@@ -3423,11 +3701,11 @@ function deriveFromRelativePaths(paths) {
|
|
|
3423
3701
|
for (const p of paths) {
|
|
3424
3702
|
const segs = p.split("/").filter(Boolean);
|
|
3425
3703
|
if (segs.length >= 2) {
|
|
3426
|
-
const d2 = segs.slice(0,
|
|
3704
|
+
const d2 = segs.slice(0, TRUNC.FINGERPRINT_SEG).join("/");
|
|
3427
3705
|
dir2Counts.set(d2, (dir2Counts.get(d2) ?? 0) + 1);
|
|
3428
3706
|
}
|
|
3429
3707
|
}
|
|
3430
|
-
const stableDirs = [...dir2Counts.entries()].sort((a, b) => b[1] - a[1]).slice(0,
|
|
3708
|
+
const stableDirs = [...dir2Counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, TRUNC.CONV_HASH).map(([d]) => d).sort();
|
|
3431
3709
|
return hashProjectId(topEntries.join(",") + "|" + stableDirs.join(","));
|
|
3432
3710
|
}
|
|
3433
3711
|
function deriveProjectId(cwd, extraction, sessionId) {
|
|
@@ -3495,7 +3773,7 @@ function loadProjectFingerprint(projectId) {
|
|
|
3495
3773
|
const data = readJsonSync(getFingerprintPath(projectId));
|
|
3496
3774
|
if (!data)
|
|
3497
3775
|
return null;
|
|
3498
|
-
if (Date.now() - data.updatedAt >
|
|
3776
|
+
if (Date.now() - data.updatedAt > THIRTY_DAYS_MS)
|
|
3499
3777
|
return null;
|
|
3500
3778
|
return data;
|
|
3501
3779
|
}
|
|
@@ -3551,7 +3829,7 @@ function extractWithCache(rc) {
|
|
|
3551
3829
|
const pruneEnd = Date.now();
|
|
3552
3830
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
3553
3831
|
const extractionStart = pruneEnd;
|
|
3554
|
-
const convText = serializeConversation(rc.llmMessages);
|
|
3832
|
+
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
3555
3833
|
const convTokens = estimateTokens(convText);
|
|
3556
3834
|
const backupPath = backupConversation(convText, rc.sessionId);
|
|
3557
3835
|
const prevContext = getPreviousCompactionContext(rc.branch);
|
|
@@ -3622,6 +3900,7 @@ function extractWithCache(rc) {
|
|
|
3622
3900
|
init_overlays();
|
|
3623
3901
|
|
|
3624
3902
|
// src/phases/explore.ts
|
|
3903
|
+
import { Type } from "typebox";
|
|
3625
3904
|
init_constants();
|
|
3626
3905
|
init_cache();
|
|
3627
3906
|
init_tokens();
|
|
@@ -3644,32 +3923,32 @@ var EXPLORATION_TOOLS = [
|
|
|
3644
3923
|
{
|
|
3645
3924
|
name: "get_message_range",
|
|
3646
3925
|
description: "Get compact summaries of messages from start to end index (0-based).",
|
|
3647
|
-
parameters: {
|
|
3926
|
+
parameters: Type.Object({ start: Type.Number(), end: Type.Number() })
|
|
3648
3927
|
},
|
|
3649
3928
|
{
|
|
3650
3929
|
name: "search_conversation",
|
|
3651
3930
|
description: "Search for text in conversation messages.",
|
|
3652
|
-
parameters: {
|
|
3931
|
+
parameters: Type.Object({ query: Type.String() })
|
|
3653
3932
|
},
|
|
3654
3933
|
{
|
|
3655
3934
|
name: "get_recent_user_messages",
|
|
3656
3935
|
description: "Get the last N user messages.",
|
|
3657
|
-
parameters: {
|
|
3936
|
+
parameters: Type.Object({ count: Type.Optional(Type.Number()) })
|
|
3658
3937
|
},
|
|
3659
3938
|
{
|
|
3660
3939
|
name: "get_context_around",
|
|
3661
3940
|
description: "Get context around a specific message index.",
|
|
3662
|
-
parameters: {
|
|
3941
|
+
parameters: Type.Object({ index: Type.Number(), radius: Type.Optional(Type.Number()) })
|
|
3663
3942
|
},
|
|
3664
3943
|
{
|
|
3665
3944
|
name: "get_file_changes",
|
|
3666
3945
|
description: "Get tool calls that modified a specific file.",
|
|
3667
|
-
parameters: {
|
|
3946
|
+
parameters: Type.Object({ path: Type.String() })
|
|
3668
3947
|
},
|
|
3669
3948
|
{
|
|
3670
3949
|
name: "get_error_chain",
|
|
3671
3950
|
description: "Get all messages related to a specific error.",
|
|
3672
|
-
parameters: {
|
|
3951
|
+
parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
|
|
3673
3952
|
}
|
|
3674
3953
|
];
|
|
3675
3954
|
function executeExplorationTool(call, llmMessages) {
|
|
@@ -3680,7 +3959,7 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
3680
3959
|
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
3681
3960
|
idx: s + i,
|
|
3682
3961
|
role: m?.role,
|
|
3683
|
-
preview: extractText(m?.content).slice(0,
|
|
3962
|
+
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW),
|
|
3684
3963
|
toolCalls: getToolCallNames(m?.content),
|
|
3685
3964
|
isError: m?.isError
|
|
3686
3965
|
})));
|
|
@@ -3703,7 +3982,7 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
3703
3982
|
return JSON.stringify(matches.map(({ idx, m }) => ({
|
|
3704
3983
|
idx,
|
|
3705
3984
|
role: m?.role,
|
|
3706
|
-
preview: extractText(m?.content).slice(0,
|
|
3985
|
+
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW)
|
|
3707
3986
|
})));
|
|
3708
3987
|
}
|
|
3709
3988
|
case "get_recent_user_messages": {
|
|
@@ -3716,7 +3995,7 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
3716
3995
|
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
3717
3996
|
idx: s + i,
|
|
3718
3997
|
role: m?.role,
|
|
3719
|
-
text: extractText(m?.content).slice(0,
|
|
3998
|
+
text: extractText(m?.content).slice(0, TRUNC.DETAIL),
|
|
3720
3999
|
toolCalls: getToolCallNames(m?.content),
|
|
3721
4000
|
isError: m?.isError
|
|
3722
4001
|
})));
|
|
@@ -3728,22 +4007,16 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
3728
4007
|
const tcs = filterToolCalls(llmMessages[i]?.content);
|
|
3729
4008
|
for (const block of tcs) {
|
|
3730
4009
|
const a = block.arguments ?? {};
|
|
3731
|
-
const fileFields = [
|
|
3732
|
-
a.path,
|
|
3733
|
-
a.file,
|
|
3734
|
-
a.filePath,
|
|
3735
|
-
a.file_path
|
|
3736
|
-
].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
|
|
4010
|
+
const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
|
|
3737
4011
|
const matchesPath = fileFields.some((f) => f.includes(target));
|
|
3738
|
-
if (block.
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
results.push({ idx: i, role: "assistant", toolCall: "write", preview: extractText(llmMessages[i]?.content).slice(0, 400) });
|
|
4012
|
+
if (classifyTool(block.arguments) === "mutates" && matchesPath) {
|
|
4013
|
+
const surgical = a.oldText != null || a.newText != null || a.edits != null || a.patch != null;
|
|
4014
|
+
const preview = extractText(llmMessages[i]?.content).slice(0, TRUNC.PREVIEW_LONG);
|
|
4015
|
+
results.push(surgical ? { idx: i, role: "assistant", toolCall: block.name ?? "mutates", args: block.arguments, preview } : { idx: i, role: "assistant", toolCall: block.name ?? "mutates", preview });
|
|
3743
4016
|
}
|
|
3744
4017
|
}
|
|
3745
4018
|
}
|
|
3746
|
-
return JSON.stringify(results.slice(0,
|
|
4019
|
+
return JSON.stringify(results.slice(0, TRUNC.EXPLORE_RESULTS) || [{ info: "No edits found for: " + args.path }]);
|
|
3747
4020
|
}
|
|
3748
4021
|
case "get_error_chain": {
|
|
3749
4022
|
const errIdx = args.index ?? 0;
|
|
@@ -3752,7 +4025,7 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
3752
4025
|
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
3753
4026
|
idx: s + i,
|
|
3754
4027
|
role: m?.role,
|
|
3755
|
-
text: extractText(m?.content).slice(0,
|
|
4028
|
+
text: extractText(m?.content).slice(0, TRUNC.PREVIEW_XL),
|
|
3756
4029
|
isError: m?.isError,
|
|
3757
4030
|
toolCalls: getToolCallNames(m?.content)
|
|
3758
4031
|
})));
|
|
@@ -3785,40 +4058,53 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
3785
4058
|
if (boundaryMatch) {
|
|
3786
4059
|
try {
|
|
3787
4060
|
const boundaries = JSON.parse("[" + boundaryMatch[1] + "]");
|
|
3788
|
-
return { ...fallbackExplorationReport(llmMessages), boundaries: boundaries
|
|
3789
|
-
afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
|
|
3790
|
-
topic: String(b.topic ?? "").slice(0, 100),
|
|
3791
|
-
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
3792
|
-
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
3793
|
-
})) };
|
|
4061
|
+
return { ...fallbackExplorationReport(llmMessages), boundaries: normalizeBoundaries(boundaries, llmMessages.length) };
|
|
3794
4062
|
} catch (err) {
|
|
3795
4063
|
debug("Boundary JSON parse failed", err);
|
|
3796
4064
|
}
|
|
3797
4065
|
}
|
|
3798
4066
|
return fallbackExplorationReport(llmMessages);
|
|
3799
4067
|
}
|
|
4068
|
+
var BOUNDARY_PRIORITIES = ["critical", "high", "normal", "low"];
|
|
4069
|
+
var SESSION_TYPES = ["implementation", "review", "debugging", "discussion"];
|
|
4070
|
+
function stringArray(v) {
|
|
4071
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
4072
|
+
}
|
|
4073
|
+
function normalizeBoundaries(raw, llmLength) {
|
|
4074
|
+
if (!Array.isArray(raw))
|
|
4075
|
+
return [];
|
|
4076
|
+
const maxIndex = Math.max(0, llmLength - 2);
|
|
4077
|
+
return raw.filter((b) => !!b && typeof b === "object" && typeof b.afterIndex === "number").map((b) => {
|
|
4078
|
+
const priority = b.priority;
|
|
4079
|
+
const confidence = b.confidence;
|
|
4080
|
+
return {
|
|
4081
|
+
afterIndex: Math.max(0, Math.min(b.afterIndex, maxIndex)),
|
|
4082
|
+
topic: String(b.topic ?? "").slice(0, TRUNC.TOPIC_LABEL),
|
|
4083
|
+
priority: typeof priority === "string" && BOUNDARY_PRIORITIES.includes(priority) ? priority : "normal",
|
|
4084
|
+
confidence: typeof confidence === "number" && Number.isFinite(confidence) ? Math.min(1, Math.max(0, confidence)) : 0.5
|
|
4085
|
+
};
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
3800
4088
|
function buildExplorationReportFromParsed(parsed, llmMessages) {
|
|
3801
4089
|
if (typeof parsed !== "object" || parsed === null) {
|
|
3802
4090
|
return fallbackExplorationReport(llmMessages);
|
|
3803
4091
|
}
|
|
4092
|
+
const p = parsed;
|
|
4093
|
+
const statusAssessment = p.statusAssessment ?? null;
|
|
4094
|
+
const sessionTypeRaw = p.sessionType;
|
|
3804
4095
|
return {
|
|
3805
|
-
boundaries: (
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
})),
|
|
3811
|
-
mainGoal: parsed.mainGoal ?? "",
|
|
3812
|
-
sessionType: ["implementation", "review", "debugging", "discussion"].includes(parsed.sessionType) ? parsed.sessionType : "implementation",
|
|
3813
|
-
enrichedConstraints: Array.isArray(parsed.enrichedConstraints) ? parsed.enrichedConstraints.map(String) : [],
|
|
3814
|
-
crossReferences: Array.isArray(parsed.crossReferences) ? parsed.crossReferences.map(String) : [],
|
|
4096
|
+
boundaries: normalizeBoundaries(p.boundaries, llmMessages.length),
|
|
4097
|
+
mainGoal: typeof p.mainGoal === "string" ? p.mainGoal : "",
|
|
4098
|
+
sessionType: typeof sessionTypeRaw === "string" && SESSION_TYPES.includes(sessionTypeRaw) ? sessionTypeRaw : "implementation",
|
|
4099
|
+
enrichedConstraints: stringArray(p.enrichedConstraints),
|
|
4100
|
+
crossReferences: stringArray(p.crossReferences),
|
|
3815
4101
|
statusAssessment: {
|
|
3816
|
-
done:
|
|
3817
|
-
inProgress:
|
|
3818
|
-
blocked:
|
|
4102
|
+
done: stringArray(statusAssessment?.done),
|
|
4103
|
+
inProgress: stringArray(statusAssessment?.inProgress),
|
|
4104
|
+
blocked: stringArray(statusAssessment?.blocked)
|
|
3819
4105
|
},
|
|
3820
|
-
criticalContext:
|
|
3821
|
-
keyDecisions:
|
|
4106
|
+
criticalContext: stringArray(p.criticalContext),
|
|
4107
|
+
keyDecisions: stringArray(p.keyDecisions)
|
|
3822
4108
|
};
|
|
3823
4109
|
}
|
|
3824
4110
|
function fallbackExplorationReport(llmMessages) {
|
|
@@ -3841,12 +4127,12 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3841
4127
|
"Main goal: " + (extraction.mainGoal ?? "unknown"),
|
|
3842
4128
|
"Files modified (" + extraction.modifiedFiles.length + "): " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none"),
|
|
3843
4129
|
"Files read (" + extraction.readFiles.length + "): " + (extraction.readFiles.join(", ") || "none"),
|
|
3844
|
-
"Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0,
|
|
3845
|
-
"Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0,
|
|
3846
|
-
"Constraints (" + extraction.constraints.length + "): " + (extraction.constraints.map((cc) => "[" + cc.category + "] " + cc.text.slice(0,
|
|
4130
|
+
"Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, TRUNC.SNIPPET) + (e.resolved ? " (resolved)" : e.retryAttempted ? " (retry attempted)" : "")).join("; ") || "none"),
|
|
4131
|
+
"Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, TRUNC.SNIPPET)).join("; ") || "none"),
|
|
4132
|
+
"Constraints (" + extraction.constraints.length + "): " + (extraction.constraints.map((cc) => "[" + cc.category + "] " + cc.text.slice(0, TRUNC.SNIPPET)).join("; ") || "none"),
|
|
3847
4133
|
"Heuristic topics (" + extraction.topics.length + "): " + (extraction.topics.map((t) => "[" + t.startIndex + "-" + t.endIndex + "] " + t.type).join("; ") || "none"),
|
|
3848
|
-
extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0,
|
|
3849
|
-
extraction.lastErrors.length ? "Last errors: " + extraction.lastErrors.map((e) => e.slice(0,
|
|
4134
|
+
extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0, TRUNC.TOPIC_LABEL)).join(" | ") : "",
|
|
4135
|
+
extraction.lastErrors.length ? "Last errors: " + extraction.lastErrors.map((e) => e.slice(0, TRUNC.TOPIC_LABEL)).join(" | ") : ""
|
|
3850
4136
|
].filter(Boolean).join(`
|
|
3851
4137
|
`);
|
|
3852
4138
|
const userContent = `Explore this conversation and produce the structured report.
|
|
@@ -3886,7 +4172,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3886
4172
|
toolSupport.set(cacheKey, true, svc.clock.now());
|
|
3887
4173
|
const messages = [
|
|
3888
4174
|
{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() },
|
|
3889
|
-
|
|
4175
|
+
probeResp
|
|
3890
4176
|
];
|
|
3891
4177
|
for (const tc of toolCalls) {
|
|
3892
4178
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
@@ -3920,7 +4206,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3920
4206
|
}
|
|
3921
4207
|
return { report: report2, rounds, toolSupported: true };
|
|
3922
4208
|
}
|
|
3923
|
-
messages.push(
|
|
4209
|
+
messages.push(response);
|
|
3924
4210
|
for (const tc of nextToolCalls) {
|
|
3925
4211
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
3926
4212
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
@@ -3961,7 +4247,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3961
4247
|
return { report, rounds: 1, toolSupported: supportsTools };
|
|
3962
4248
|
}
|
|
3963
4249
|
async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal, services) {
|
|
3964
|
-
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0,
|
|
4250
|
+
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.PREVIEW)).join(`
|
|
3965
4251
|
`);
|
|
3966
4252
|
const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
|
|
3967
4253
|
|
|
@@ -3987,10 +4273,10 @@ User steering: ` + userNote : "");
|
|
|
3987
4273
|
}
|
|
3988
4274
|
}
|
|
3989
4275
|
async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal, services) {
|
|
3990
|
-
const first3 = llmMessages.filter((m) => m?.role === "user").slice(0, 3).map((m) => extractText(m?.content).slice(0,
|
|
4276
|
+
const first3 = llmMessages.filter((m) => m?.role === "user").slice(0, 3).map((m) => extractText(m?.content).slice(0, TRUNC.PREVIEW_MID)).join(`
|
|
3991
4277
|
---
|
|
3992
4278
|
`);
|
|
3993
|
-
const last30 = llmMessages.slice(-30).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0,
|
|
4279
|
+
const last30 = llmMessages.slice(-30).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.DETAIL)).join(`
|
|
3994
4280
|
`);
|
|
3995
4281
|
const prompt = `Analyze this conversation and produce a JSON report.
|
|
3996
4282
|
|
|
@@ -3999,9 +4285,9 @@ First user messages:
|
|
|
3999
4285
|
|
|
4000
4286
|
Deterministic data:
|
|
4001
4287
|
` + "- Files modified: " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none") + `
|
|
4002
|
-
- Errors: ` + (extraction.errors.map((e) => e.message.slice(0,
|
|
4003
|
-
- Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0,
|
|
4004
|
-
- Constraints: ` + (extraction.constraints.map((c) => c.text.slice(0,
|
|
4288
|
+
- Errors: ` + (extraction.errors.map((e) => e.message.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
|
|
4289
|
+
- Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
|
|
4290
|
+
- Constraints: ` + (extraction.constraints.map((c) => c.text.slice(0, TRUNC.SNIPPET)).join("; ") || "none") + `
|
|
4005
4291
|
|
|
4006
4292
|
Last 30 messages:
|
|
4007
4293
|
` + last30 + (prevSummary ? `
|
|
@@ -4114,7 +4400,7 @@ Session-specific instructions:
|
|
|
4114
4400
|
async function summarizeBatch(batch, extraction, model, auth, signal, services) {
|
|
4115
4401
|
const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
|
|
4116
4402
|
const extractionCtx = buildExtractionContext(extraction, range);
|
|
4117
|
-
const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0,
|
|
4403
|
+
const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0, TRUNC.OPEN_LOOP_SUMMARY) + (d.userResponse ? " \u2192 " + d.userResponse.slice(0, TRUNC.DECISION_DETAIL) : ""));
|
|
4118
4404
|
const decisionCtx = activeDecisions.length ? `
|
|
4119
4405
|
## Active Decisions from previous segments (honour these):
|
|
4120
4406
|
` + activeDecisions.join(`
|
|
@@ -4124,8 +4410,8 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
|
|
|
4124
4410
|
return "--- CHUNK " + id + ": " + ch.topic + " (" + ch.priority + `) ---
|
|
4125
4411
|
` + ch.messages.map((m) => {
|
|
4126
4412
|
const role = m?.role ?? "unknown";
|
|
4127
|
-
const content = extractText(m?.content).slice(0,
|
|
4128
|
-
const toolCalls = filterToolCalls(m?.content).map((tc) => tc.name + " " + JSON.stringify(tc.arguments).slice(0,
|
|
4413
|
+
const content = extractText(m?.content).slice(0, TRUNC.PREVIEW_XL);
|
|
4414
|
+
const toolCalls = filterToolCalls(m?.content).map((tc) => tc.name + " " + JSON.stringify(tc.arguments).slice(0, TRUNC.DETAIL)).join("; ");
|
|
4129
4415
|
const toolSuffix = toolCalls ? `
|
|
4130
4416
|
[tool_calls] ` + toolCalls : "";
|
|
4131
4417
|
return "[" + role + "] " + content + toolSuffix;
|
|
@@ -4167,9 +4453,9 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services)
|
|
|
4167
4453
|
const prio = f("Priority").toLowerCase();
|
|
4168
4454
|
const sectionFallback = sec.split(`
|
|
4169
4455
|
`).slice(1).join(`
|
|
4170
|
-
`).trim().slice(0,
|
|
4171
|
-
const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0,
|
|
4172
|
-
`).slice(0,
|
|
4456
|
+
`).trim().slice(0, TRUNC.PREVIEW_XL);
|
|
4457
|
+
const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0, TRUNC.CHUNK_FALLBACK)).join(`
|
|
4458
|
+
`).slice(0, TRUNC.PREVIEW_XL);
|
|
4173
4459
|
return {
|
|
4174
4460
|
topic: ch.topic,
|
|
4175
4461
|
startIndex: ch.startIndex,
|
|
@@ -4206,18 +4492,18 @@ function assembleFallback(summaries, extraction) {
|
|
|
4206
4492
|
extraction.mainGoal ?? "See topics below.",
|
|
4207
4493
|
"",
|
|
4208
4494
|
"## Constraints & Preferences",
|
|
4209
|
-
...extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0,
|
|
4495
|
+
...extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, TRUNC.PREVIEW_MID)),
|
|
4210
4496
|
"",
|
|
4211
4497
|
"## Progress",
|
|
4212
4498
|
"### Done",
|
|
4213
4499
|
"- See topics below",
|
|
4214
4500
|
"### In Progress",
|
|
4215
|
-
...summaries.filter((s) => s.priority === "high").map((s) => "- [ ] " + s.summary.slice(0,
|
|
4501
|
+
...summaries.filter((s) => s.priority === "high").map((s) => "- [ ] " + s.summary.slice(0, TRUNC.PREVIEW)),
|
|
4216
4502
|
"### Blocked",
|
|
4217
4503
|
"- None",
|
|
4218
4504
|
"",
|
|
4219
4505
|
"## Key Decisions",
|
|
4220
|
-
...extraction.decisions.map((d) => "- **" + d.summary.slice(0,
|
|
4506
|
+
...extraction.decisions.map((d) => "- **" + d.summary.slice(0, TRUNC.TOPIC_LABEL) + "**" + (d.userResponse ? " \u2192 " + d.userResponse : "")),
|
|
4221
4507
|
"",
|
|
4222
4508
|
"## Files Modified",
|
|
4223
4509
|
...detModified.map((f) => "- " + f),
|
|
@@ -4229,13 +4515,26 @@ function assembleFallback(summaries, extraction) {
|
|
|
4229
4515
|
"1. See topics below",
|
|
4230
4516
|
"",
|
|
4231
4517
|
"## Critical Context",
|
|
4232
|
-
...extraction.errors.filter((e) => !e.resolved).map((e) => "- Unresolved error: " + e.message.slice(0,
|
|
4518
|
+
...extraction.errors.filter((e) => !e.resolved).map((e) => "- Unresolved error: " + e.message.slice(0, TRUNC.TOPIC_LABEL)),
|
|
4233
4519
|
"",
|
|
4234
4520
|
"## Topics Covered",
|
|
4235
|
-
...summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0,
|
|
4521
|
+
...summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, TRUNC.PREVIEW_MID))
|
|
4236
4522
|
].join(`
|
|
4237
4523
|
`);
|
|
4238
4524
|
}
|
|
4525
|
+
function failedChunkSummary(ch) {
|
|
4526
|
+
return {
|
|
4527
|
+
topic: ch.topic,
|
|
4528
|
+
startIndex: ch.startIndex,
|
|
4529
|
+
endIndex: ch.endIndex,
|
|
4530
|
+
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
4531
|
+
`).slice(0, TRUNC.DETAIL),
|
|
4532
|
+
keyDecisions: [],
|
|
4533
|
+
filesModified: [],
|
|
4534
|
+
filesRead: [],
|
|
4535
|
+
priority: ch.priority
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4239
4538
|
|
|
4240
4539
|
// src/app/steps/synthesize.ts
|
|
4241
4540
|
init_constants();
|
|
@@ -4354,10 +4653,15 @@ async function summarizeConversation(rc) {
|
|
|
4354
4653
|
}
|
|
4355
4654
|
const concurrency = rc.providerCaps.concurrencyLimit;
|
|
4356
4655
|
if (totalBatches <= 1) {
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4656
|
+
const single = batches[0];
|
|
4657
|
+
if (single) {
|
|
4658
|
+
try {
|
|
4659
|
+
summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, rc.summaryAuth, rc.cancellation.signal, rc.services));
|
|
4660
|
+
} catch (err) {
|
|
4661
|
+
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
4662
|
+
}
|
|
4663
|
+
} else {
|
|
4664
|
+
rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
|
|
4361
4665
|
}
|
|
4362
4666
|
} else {
|
|
4363
4667
|
const results = new Array(totalBatches);
|
|
@@ -4434,19 +4738,6 @@ async function summarizeConversation(rc) {
|
|
|
4434
4738
|
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
4435
4739
|
return advance(out, "_synthesized");
|
|
4436
4740
|
}
|
|
4437
|
-
function failedChunkSummary(ch) {
|
|
4438
|
-
return {
|
|
4439
|
-
topic: ch.topic,
|
|
4440
|
-
startIndex: ch.startIndex,
|
|
4441
|
-
endIndex: ch.endIndex,
|
|
4442
|
-
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
4443
|
-
`).slice(0, 300),
|
|
4444
|
-
keyDecisions: [],
|
|
4445
|
-
filesModified: [],
|
|
4446
|
-
filesRead: [],
|
|
4447
|
-
priority: ch.priority
|
|
4448
|
-
};
|
|
4449
|
-
}
|
|
4450
4741
|
|
|
4451
4742
|
// src/phases/verify.ts
|
|
4452
4743
|
init_constants();
|
|
@@ -4639,6 +4930,16 @@ function appendToSection(summary, kind, text, fallbackBody = "") {
|
|
|
4639
4930
|
` : "") + text.trim());
|
|
4640
4931
|
}
|
|
4641
4932
|
|
|
4933
|
+
// src/domain/keywords.ts
|
|
4934
|
+
function extractCheckKeywords(text, max) {
|
|
4935
|
+
const words = text.split(/\s+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter((w) => w.length > 3);
|
|
4936
|
+
if (!words.length)
|
|
4937
|
+
return [];
|
|
4938
|
+
const salient = (w) => /^[A-Z\u00C0-\u00DE]/.test(w) || /[0-9]/.test(w);
|
|
4939
|
+
const preferred = words.filter(salient);
|
|
4940
|
+
return (preferred.length ? preferred : words).slice(0, max);
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4642
4943
|
// src/phases/verify.ts
|
|
4643
4944
|
function verifySummary(summary, extraction) {
|
|
4644
4945
|
const parsed = parseSummary(summary);
|
|
@@ -4670,22 +4971,22 @@ function verifySummary(summary, extraction) {
|
|
|
4670
4971
|
}
|
|
4671
4972
|
}
|
|
4672
4973
|
for (const e of extraction.errors.filter((e2) => !e2.resolved)) {
|
|
4673
|
-
const snippet = e.message.slice(0,
|
|
4974
|
+
const snippet = e.message.trim().replace(/\s+/g, " ").slice(0, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
4674
4975
|
if (snippet.length > 5 && !lower.includes(snippet)) {
|
|
4675
|
-
gaps.push("Missing error: " + e.message.slice(0,
|
|
4976
|
+
gaps.push("Missing error: " + e.message.slice(0, TRUNC.SNIPPET));
|
|
4676
4977
|
score -= 5;
|
|
4677
4978
|
}
|
|
4678
4979
|
}
|
|
4679
4980
|
for (const c of extraction.constraints.filter((c2) => c2.confidence >= 0.8)) {
|
|
4680
|
-
const keywords = c.text
|
|
4981
|
+
const keywords = extractCheckKeywords(c.text, 3);
|
|
4681
4982
|
const found = keywords.some((k) => lower.includes(k.toLowerCase()));
|
|
4682
4983
|
if (!found && keywords.length > 0) {
|
|
4683
|
-
gaps.push("Missing constraint: " + c.text.slice(0,
|
|
4984
|
+
gaps.push("Missing constraint: " + c.text.slice(0, TRUNC.TOPIC_LABEL));
|
|
4684
4985
|
score -= 3;
|
|
4685
4986
|
}
|
|
4686
4987
|
}
|
|
4687
4988
|
if (extraction.mainGoal) {
|
|
4688
|
-
const goalWords = extraction.mainGoal
|
|
4989
|
+
const goalWords = extractCheckKeywords(extraction.mainGoal, 4);
|
|
4689
4990
|
const goalFound = goalWords.some((w) => lower.includes(w.toLowerCase()));
|
|
4690
4991
|
if (!goalFound) {
|
|
4691
4992
|
gaps.push("Main goal may be missing from summary");
|
|
@@ -4728,9 +5029,9 @@ function verifySummary(summary, extraction) {
|
|
|
4728
5029
|
const decisionSection = findSection(parsed, "decisions");
|
|
4729
5030
|
const decisionBody = decisionSection?.body.toLowerCase() ?? "";
|
|
4730
5031
|
for (const d of highConfDecisions) {
|
|
4731
|
-
const keywords = d.summary
|
|
5032
|
+
const keywords = extractCheckKeywords(d.summary, 3);
|
|
4732
5033
|
if (keywords.length > 0 && !keywords.some((k) => decisionBody.includes(k.toLowerCase()))) {
|
|
4733
|
-
gaps.push("Missing decision: " + d.summary.slice(0,
|
|
5034
|
+
gaps.push("Missing decision: " + d.summary.slice(0, TRUNC.TOPIC_LABEL));
|
|
4734
5035
|
score -= 3;
|
|
4735
5036
|
}
|
|
4736
5037
|
}
|
|
@@ -4909,7 +5210,7 @@ function loadCompactionState(projectId) {
|
|
|
4909
5210
|
updatedAt = 0;
|
|
4910
5211
|
}
|
|
4911
5212
|
}
|
|
4912
|
-
if (Date.now() - updatedAt >
|
|
5213
|
+
if (Date.now() - updatedAt > SEVEN_DAYS_MS)
|
|
4913
5214
|
return null;
|
|
4914
5215
|
}
|
|
4915
5216
|
return data;
|
|
@@ -4918,17 +5219,18 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4918
5219
|
let decisionId = 0;
|
|
4919
5220
|
let constraintId = 0;
|
|
4920
5221
|
let errorId = 0;
|
|
5222
|
+
const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
|
|
4921
5223
|
return {
|
|
4922
5224
|
goal: extraction.mainGoal,
|
|
4923
5225
|
decisions: extraction.decisions.map((d) => ({
|
|
4924
|
-
id:
|
|
4925
|
-
summary: d.summary.slice(0,
|
|
4926
|
-
...d.userResponse ? { userResponse: d.userResponse.slice(0,
|
|
5226
|
+
id: ID_PREFIX.DECISION + ++decisionId,
|
|
5227
|
+
summary: d.summary.slice(0, TRUNC.DECISION_SUMMARY),
|
|
5228
|
+
...d.userResponse ? { userResponse: d.userResponse.slice(0, TRUNC.USER_RESPONSE) } : {},
|
|
4927
5229
|
type: d.type
|
|
4928
5230
|
})),
|
|
4929
5231
|
constraints: extraction.constraints.map((c) => ({
|
|
4930
5232
|
id: "constraint-" + ++constraintId,
|
|
4931
|
-
text: c.text.slice(0,
|
|
5233
|
+
text: c.text.slice(0, TRUNC.CONSTRAINT_TEXT),
|
|
4932
5234
|
category: c.category,
|
|
4933
5235
|
confidence: c.confidence
|
|
4934
5236
|
})),
|
|
@@ -4936,17 +5238,18 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4936
5238
|
readFiles: extraction.readFiles,
|
|
4937
5239
|
deletedFiles: extraction.deletedFiles,
|
|
4938
5240
|
unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
|
|
4939
|
-
const
|
|
5241
|
+
const msgLower = e.message.toLowerCase();
|
|
5242
|
+
const match = fileNeedles.find(({ needles }) => needles.some((n) => msgLower.includes(n)));
|
|
4940
5243
|
return {
|
|
4941
|
-
id:
|
|
4942
|
-
message: e.message.slice(0,
|
|
5244
|
+
id: ID_PREFIX.ERROR + ++errorId,
|
|
5245
|
+
message: e.message.slice(0, TRUNC.MESSAGE),
|
|
4943
5246
|
tool: e.tool,
|
|
4944
|
-
files:
|
|
5247
|
+
files: match ? [match.path] : []
|
|
4945
5248
|
};
|
|
4946
5249
|
}),
|
|
4947
5250
|
resolvedErrors: extraction.errors.filter((e) => e.resolved).map((e) => ({
|
|
4948
|
-
id:
|
|
4949
|
-
message: e.message.slice(0,
|
|
5251
|
+
id: ID_PREFIX.ERROR + ++errorId,
|
|
5252
|
+
message: e.message.slice(0, TRUNC.MESSAGE),
|
|
4950
5253
|
tool: e.tool
|
|
4951
5254
|
})),
|
|
4952
5255
|
openLoops,
|
|
@@ -4976,28 +5279,28 @@ function injectOpenLoopsSection(summary, openLoops) {
|
|
|
4976
5279
|
return renderSummary(updated);
|
|
4977
5280
|
}
|
|
4978
5281
|
function computeDelta(prev, current) {
|
|
4979
|
-
const
|
|
4980
|
-
const
|
|
4981
|
-
const
|
|
4982
|
-
const
|
|
4983
|
-
const
|
|
4984
|
-
const
|
|
5282
|
+
const key = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
5283
|
+
const prevDecisionTexts = new Set(prev.decisions.map((d) => key(d.summary)));
|
|
5284
|
+
const currDecisionTexts = new Set(current.decisions.map((d) => key(d.summary)));
|
|
5285
|
+
const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(key(d.summary))).map((d) => d.summary);
|
|
5286
|
+
const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(key(d.summary))).map((d) => d.summary);
|
|
5287
|
+
const prevLoopSummaries = new Map(prev.openLoops.map((l) => [key(l.summary), l]));
|
|
5288
|
+
const currLoopKeys = new Set(current.openLoops.map((l) => key(l.summary)));
|
|
4985
5289
|
const resolvedLoops = [];
|
|
4986
5290
|
const persistentLoops = [];
|
|
4987
|
-
for (const [
|
|
4988
|
-
if (
|
|
5291
|
+
for (const [k, loop] of prevLoopSummaries) {
|
|
5292
|
+
if (currLoopKeys.has(k))
|
|
4989
5293
|
persistentLoops.push(loop.summary);
|
|
4990
|
-
|
|
5294
|
+
else
|
|
4991
5295
|
resolvedLoops.push(loop.summary);
|
|
4992
|
-
}
|
|
4993
5296
|
}
|
|
4994
|
-
const newLoops = current.openLoops.filter((l) => !prevLoopSummaries.has(l.summary
|
|
5297
|
+
const newLoops = current.openLoops.filter((l) => !prevLoopSummaries.has(key(l.summary))).map((l) => l.summary);
|
|
4995
5298
|
const prevFiles = new Set(prev.modifiedFiles);
|
|
4996
5299
|
const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
|
|
4997
|
-
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => e.message
|
|
4998
|
-
const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => e.message
|
|
4999
|
-
const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(e.message
|
|
5000
|
-
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(e.message
|
|
5300
|
+
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => key(e.message)));
|
|
5301
|
+
const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => key(e.message)));
|
|
5302
|
+
const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(key(e.message))).map((e) => e.message);
|
|
5303
|
+
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(key(e.message))).map((e) => e.message);
|
|
5001
5304
|
const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
|
|
5002
5305
|
return {
|
|
5003
5306
|
newDecisions,
|
|
@@ -5018,22 +5321,22 @@ function formatDeltaSection(delta) {
|
|
|
5018
5321
|
lines.push("- **Goal shifted**: " + (delta.previousGoal ?? "?") + " \u2192 see current goal above");
|
|
5019
5322
|
}
|
|
5020
5323
|
if (delta.resolvedLoops.length) {
|
|
5021
|
-
lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0,
|
|
5324
|
+
lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0, TRUNC.DECISION_DETAIL) + "~~").join(", "));
|
|
5022
5325
|
}
|
|
5023
5326
|
if (delta.persistentLoops.length) {
|
|
5024
|
-
lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0,
|
|
5327
|
+
lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
|
|
5025
5328
|
}
|
|
5026
5329
|
if (delta.newLoops.length) {
|
|
5027
|
-
lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0,
|
|
5330
|
+
lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
|
|
5028
5331
|
}
|
|
5029
5332
|
if (delta.newDecisions.length) {
|
|
5030
|
-
lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0,
|
|
5333
|
+
lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, TRUNC.SNIPPET)).join("; "));
|
|
5031
5334
|
}
|
|
5032
5335
|
if (delta.resolvedErrors.length) {
|
|
5033
|
-
lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0,
|
|
5336
|
+
lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
|
|
5034
5337
|
}
|
|
5035
5338
|
if (delta.newErrors.length) {
|
|
5036
|
-
lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0,
|
|
5339
|
+
lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
|
|
5037
5340
|
}
|
|
5038
5341
|
if (delta.newModifiedFiles.length) {
|
|
5039
5342
|
lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
|
|
@@ -5055,81 +5358,38 @@ function injectDeltaSection(summary, delta) {
|
|
|
5055
5358
|
const updated = upsertSection(parsed, "changes", body, placement);
|
|
5056
5359
|
return renderSummary(updated);
|
|
5057
5360
|
}
|
|
5361
|
+
function ensurePinnedPaths(summary, pinned) {
|
|
5362
|
+
if (!pinned.length)
|
|
5363
|
+
return summary;
|
|
5364
|
+
const lower = summary.toLowerCase();
|
|
5365
|
+
const missing = pinned.filter((p) => p && p.trim().length > 0 && !lower.includes(p.toLowerCase()));
|
|
5366
|
+
if (!missing.length)
|
|
5367
|
+
return summary;
|
|
5368
|
+
const parsed = parseSummary(summary);
|
|
5369
|
+
const updated = appendToSection(parsed, "files-read", missing.map((p) => "- " + p).join(`
|
|
5370
|
+
`), "- Pinned by config (always preserved):");
|
|
5371
|
+
return renderSummary(updated);
|
|
5372
|
+
}
|
|
5058
5373
|
function extractNextActions(summary) {
|
|
5059
|
-
const
|
|
5060
|
-
if (!
|
|
5374
|
+
const section = findSection(summary, "next-steps");
|
|
5375
|
+
if (!section)
|
|
5061
5376
|
return [];
|
|
5062
|
-
return
|
|
5377
|
+
return section.body.split(`
|
|
5063
5378
|
`).map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5064
5379
|
}
|
|
5065
5380
|
function extractCriticalContext(summary) {
|
|
5066
|
-
const
|
|
5067
|
-
if (!
|
|
5381
|
+
const section = findSection(summary, "critical-context");
|
|
5382
|
+
if (!section)
|
|
5068
5383
|
return [];
|
|
5069
|
-
return
|
|
5384
|
+
return section.body.split(`
|
|
5070
5385
|
`).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5071
5386
|
}
|
|
5072
5387
|
|
|
5073
|
-
// src/app/steps/state.ts
|
|
5074
|
-
init_tokens();
|
|
5075
|
-
function buildState(rc) {
|
|
5076
|
-
const extraction = rc.extraction;
|
|
5077
|
-
let summary = rc.finalSummary;
|
|
5078
|
-
const openLoops = extractOpenLoops(rc.llmMessages, extraction);
|
|
5079
|
-
if (openLoops.length > 0) {
|
|
5080
|
-
rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
5081
|
-
summary = injectOpenLoopsSection(summary, openLoops);
|
|
5082
|
-
}
|
|
5083
|
-
const nextActions = extractNextActions(summary);
|
|
5084
|
-
const criticalContextItems = extractCriticalContext(summary);
|
|
5085
|
-
const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
|
|
5086
|
-
const prevState = loadCompactionState(rc.projectId);
|
|
5087
|
-
if (prevState) {
|
|
5088
|
-
const delta = computeDelta(prevState, compactionState);
|
|
5089
|
-
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
5090
|
-
summary = injectDeltaSection(summary, delta);
|
|
5091
|
-
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
5095
|
-
const detRead = extraction.readFiles;
|
|
5096
|
-
const estimatedAfter = estimateTokens(summary) + rc.accTokens;
|
|
5097
|
-
const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
|
|
5098
|
-
const details = {
|
|
5099
|
-
method: rc.method,
|
|
5100
|
-
chunkCount: rc.chunkCount || 1,
|
|
5101
|
-
topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
|
|
5102
|
-
readFiles: detRead,
|
|
5103
|
-
modifiedFiles: detModified,
|
|
5104
|
-
totalMessages: rc.toCompact.length,
|
|
5105
|
-
totalTokensSummarized: rc.convTokens,
|
|
5106
|
-
llmCalls: rc.llmCalls,
|
|
5107
|
-
profile: rc.profile,
|
|
5108
|
-
backupPath: rc.backupPath,
|
|
5109
|
-
tokensSaved,
|
|
5110
|
-
verified: rc.verified,
|
|
5111
|
-
gaps: rc.verificationGaps,
|
|
5112
|
-
explorationRounds: rc.explorationRounds,
|
|
5113
|
-
explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
|
|
5114
|
-
model: rc.modelLabel,
|
|
5115
|
-
qualityScore: rc.verificationScore,
|
|
5116
|
-
tokensBefore: rc.totalTokens,
|
|
5117
|
-
compactionState,
|
|
5118
|
-
openLoops
|
|
5119
|
-
};
|
|
5120
|
-
const out = rc;
|
|
5121
|
-
out.finalSummary = summary;
|
|
5122
|
-
out.openLoops = openLoops;
|
|
5123
|
-
out.compactionState = compactionState;
|
|
5124
|
-
out.details = details;
|
|
5125
|
-
out.tokensSaved = tokensSaved;
|
|
5126
|
-
return advance(out, "_stated");
|
|
5127
|
-
}
|
|
5128
|
-
|
|
5129
5388
|
// src/utils/damage.ts
|
|
5130
5389
|
init_logger();
|
|
5131
5390
|
init_paths();
|
|
5132
5391
|
init_fs();
|
|
5392
|
+
init_constants();
|
|
5133
5393
|
var COMPLAINT_PATTERNS = [
|
|
5134
5394
|
/(?:I already (?:told|said|mentioned|explained) you|(?:we|I) (?:already|just) (?:discussed|went over|covered) this|you forgot|you lost|nerede kald\u0131|hat\u0131rlam\u0131yor|unuttun)/i,
|
|
5135
5395
|
/(?:that'?s? not (?:what I|right)|that'?s? wrong|yanl\u0131\u015F|hay\u0131r de\u011Fil|no that'|that doesn'?t match)/i,
|
|
@@ -5137,6 +5397,7 @@ var COMPLAINT_PATTERNS = [
|
|
|
5137
5397
|
];
|
|
5138
5398
|
function detectDamage(postMessages, details) {
|
|
5139
5399
|
const signals = [];
|
|
5400
|
+
const reReadFiles = [];
|
|
5140
5401
|
const compactedFiles = new Set(details.modifiedFiles.map((f) => f.toLowerCase()));
|
|
5141
5402
|
const compactedReadFiles = new Set(details.readFiles.map((f) => f.toLowerCase()));
|
|
5142
5403
|
for (let i = 0;i < postMessages.length; i++) {
|
|
@@ -5145,8 +5406,8 @@ function detectDamage(postMessages, details) {
|
|
|
5145
5406
|
if (msg.role === "assistant") {
|
|
5146
5407
|
const blocks = Array.isArray(msg.content) ? msg.content : [];
|
|
5147
5408
|
for (const b of blocks) {
|
|
5148
|
-
if (isToolCallBlock(b) && (b.
|
|
5149
|
-
const fp = b.arguments
|
|
5409
|
+
if (isToolCallBlock(b) && classifyTool(b.arguments) === "accesses") {
|
|
5410
|
+
const fp = extractToolPath(b.arguments);
|
|
5150
5411
|
if (fp) {
|
|
5151
5412
|
const fpLower = fp.toLowerCase();
|
|
5152
5413
|
if (compactedFiles.has(fpLower) || compactedReadFiles.has(fpLower)) {
|
|
@@ -5155,6 +5416,8 @@ function detectDamage(postMessages, details) {
|
|
|
5155
5416
|
severity: "medium",
|
|
5156
5417
|
detail: "Agent re-read compacted file: " + fp
|
|
5157
5418
|
});
|
|
5419
|
+
if (!reReadFiles.includes(fp))
|
|
5420
|
+
reReadFiles.push(fp);
|
|
5158
5421
|
}
|
|
5159
5422
|
}
|
|
5160
5423
|
}
|
|
@@ -5166,18 +5429,18 @@ function detectDamage(postMessages, details) {
|
|
|
5166
5429
|
signals.push({
|
|
5167
5430
|
type: "user-complaint",
|
|
5168
5431
|
severity: "high",
|
|
5169
|
-
detail: 'User complaint after compaction: "' + text.slice(0,
|
|
5432
|
+
detail: 'User complaint after compaction: "' + text.slice(0, TRUNC.TOPIC_LABEL) + '"'
|
|
5170
5433
|
});
|
|
5171
5434
|
break;
|
|
5172
5435
|
}
|
|
5173
5436
|
}
|
|
5174
5437
|
for (const t of details.topics) {
|
|
5175
|
-
const topicWords = t
|
|
5176
|
-
if (topicWords.length
|
|
5438
|
+
const topicWords = extractCheckKeywords(t, 3);
|
|
5439
|
+
if (topicWords.length > 0 && topicWords.some((w) => text.includes(w.toLowerCase()))) {
|
|
5177
5440
|
signals.push({
|
|
5178
5441
|
type: "re-question",
|
|
5179
5442
|
severity: "low",
|
|
5180
|
-
detail: "User mentions compacted topic: " + t.slice(0,
|
|
5443
|
+
detail: "User mentions compacted topic: " + t.slice(0, TRUNC.SNIPPET)
|
|
5181
5444
|
});
|
|
5182
5445
|
}
|
|
5183
5446
|
}
|
|
@@ -5206,7 +5469,8 @@ function detectDamage(postMessages, details) {
|
|
|
5206
5469
|
return {
|
|
5207
5470
|
signals,
|
|
5208
5471
|
damageScore,
|
|
5209
|
-
summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)"
|
|
5472
|
+
summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)",
|
|
5473
|
+
reReadFiles
|
|
5210
5474
|
};
|
|
5211
5475
|
}
|
|
5212
5476
|
function logDamageReport(sessionId, report, details) {
|
|
@@ -5226,6 +5490,92 @@ function logDamageReport(sessionId, report, details) {
|
|
|
5226
5490
|
warn("logDamageReport failed", e);
|
|
5227
5491
|
}
|
|
5228
5492
|
}
|
|
5493
|
+
var REMEDIATION_TTL_MS = SEVEN_DAYS_MS;
|
|
5494
|
+
function writeRemediationHints(projectId, files) {
|
|
5495
|
+
if (!files.length)
|
|
5496
|
+
return;
|
|
5497
|
+
const cleaned = [...new Set(files.map((f) => (f ?? "").trim()).filter((f) => f.length > 0))];
|
|
5498
|
+
if (!cleaned.length)
|
|
5499
|
+
return;
|
|
5500
|
+
try {
|
|
5501
|
+
writeJsonSync(remediationHintsFile(projectId), { files: cleaned, updatedAt: Date.now() });
|
|
5502
|
+
} catch (e) {
|
|
5503
|
+
warn("writeRemediationHints failed", e);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
function readRemediationHints(projectId) {
|
|
5507
|
+
const data = readJsonSync(remediationHintsFile(projectId));
|
|
5508
|
+
if (!data || !Array.isArray(data.files))
|
|
5509
|
+
return [];
|
|
5510
|
+
if (typeof data.updatedAt === "number" && Date.now() - data.updatedAt > REMEDIATION_TTL_MS)
|
|
5511
|
+
return [];
|
|
5512
|
+
return data.files.filter((f) => typeof f === "string");
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
// src/app/steps/state.ts
|
|
5516
|
+
init_tokens();
|
|
5517
|
+
function buildState(rc) {
|
|
5518
|
+
const extraction = rc.extraction;
|
|
5519
|
+
let summary = rc.finalSummary;
|
|
5520
|
+
const openLoops = extractOpenLoops(rc.llmMessages, extraction);
|
|
5521
|
+
if (openLoops.length > 0) {
|
|
5522
|
+
rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
5523
|
+
summary = injectOpenLoopsSection(summary, openLoops);
|
|
5524
|
+
}
|
|
5525
|
+
const pinPaths = rc.config.pinPaths ?? [];
|
|
5526
|
+
const remediated = readRemediationHints(rc.projectId);
|
|
5527
|
+
const preserve = remediated.length ? Array.from(new Set([...pinPaths, ...remediated])) : pinPaths;
|
|
5528
|
+
if (remediated.length) {
|
|
5529
|
+
rc.notify("Remediation: re-preserving " + remediated.length + " file(s) lost in a prior compaction", "info");
|
|
5530
|
+
}
|
|
5531
|
+
if (preserve.length > 0) {
|
|
5532
|
+
summary = ensurePinnedPaths(summary, preserve);
|
|
5533
|
+
}
|
|
5534
|
+
const nextActions = extractNextActions(summary);
|
|
5535
|
+
const criticalContextItems = extractCriticalContext(summary);
|
|
5536
|
+
const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
|
|
5537
|
+
const prevState = loadCompactionState(rc.projectId);
|
|
5538
|
+
if (prevState) {
|
|
5539
|
+
const delta = computeDelta(prevState, compactionState);
|
|
5540
|
+
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
5541
|
+
summary = injectDeltaSection(summary, delta);
|
|
5542
|
+
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
5546
|
+
const detRead = extraction.readFiles;
|
|
5547
|
+
const estimatedAfter = estimateTokens(summary) + rc.accTokens;
|
|
5548
|
+
const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
|
|
5549
|
+
const details = {
|
|
5550
|
+
method: rc.method,
|
|
5551
|
+
chunkCount: rc.chunkCount || 1,
|
|
5552
|
+
topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
|
|
5553
|
+
readFiles: detRead,
|
|
5554
|
+
modifiedFiles: detModified,
|
|
5555
|
+
totalMessages: rc.toCompact.length,
|
|
5556
|
+
totalTokensSummarized: rc.convTokens,
|
|
5557
|
+
llmCalls: rc.llmCalls,
|
|
5558
|
+
profile: rc.profile,
|
|
5559
|
+
backupPath: rc.backupPath,
|
|
5560
|
+
tokensSaved,
|
|
5561
|
+
verified: rc.verified,
|
|
5562
|
+
gaps: rc.verificationGaps,
|
|
5563
|
+
explorationRounds: rc.explorationRounds,
|
|
5564
|
+
explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
|
|
5565
|
+
model: rc.modelLabel,
|
|
5566
|
+
qualityScore: rc.verificationScore,
|
|
5567
|
+
tokensBefore: rc.totalTokens,
|
|
5568
|
+
compactionState,
|
|
5569
|
+
openLoops
|
|
5570
|
+
};
|
|
5571
|
+
const out = rc;
|
|
5572
|
+
out.finalSummary = summary;
|
|
5573
|
+
out.openLoops = openLoops;
|
|
5574
|
+
out.compactionState = compactionState;
|
|
5575
|
+
out.details = details;
|
|
5576
|
+
out.tokensSaved = tokensSaved;
|
|
5577
|
+
return advance(out, "_stated");
|
|
5578
|
+
}
|
|
5229
5579
|
|
|
5230
5580
|
// src/app/steps/persist.ts
|
|
5231
5581
|
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -5236,7 +5586,7 @@ function persistDurableState(rc) {
|
|
|
5236
5586
|
}
|
|
5237
5587
|
function runDamageDetection(rc) {
|
|
5238
5588
|
try {
|
|
5239
|
-
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([e.message])).flat();
|
|
5589
|
+
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([asBranchMessage(e.message)])).flat();
|
|
5240
5590
|
if (postCompactMsgs.length <= 2)
|
|
5241
5591
|
return;
|
|
5242
5592
|
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
@@ -5252,6 +5602,9 @@ function runDamageDetection(rc) {
|
|
|
5252
5602
|
rc.notify("Previous compaction damage: " + damage.summary, "warning");
|
|
5253
5603
|
}
|
|
5254
5604
|
logDamageReport(rc.sessionId, damage, safeDetails);
|
|
5605
|
+
if (damage.reReadFiles.length > 0) {
|
|
5606
|
+
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
5607
|
+
}
|
|
5255
5608
|
} catch (err) {
|
|
5256
5609
|
warn("Damage detection error", err);
|
|
5257
5610
|
}
|
|
@@ -5461,8 +5814,16 @@ async function runSmartCompact(opts) {
|
|
|
5461
5814
|
recordSuccessMetrics(stated, "success");
|
|
5462
5815
|
if (!stated.flags.autoTriggered) {
|
|
5463
5816
|
try {
|
|
5464
|
-
|
|
5465
|
-
|
|
5817
|
+
let resultTimer;
|
|
5818
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
5819
|
+
resultTimer = setTimeout(resolve, 5000);
|
|
5820
|
+
});
|
|
5821
|
+
try {
|
|
5822
|
+
await Promise.race([showResultScreen(stated.ctx, stated.details, stated.extraction, stated.services), timeoutPromise]);
|
|
5823
|
+
} finally {
|
|
5824
|
+
if (resultTimer)
|
|
5825
|
+
clearTimeout(resultTimer);
|
|
5826
|
+
}
|
|
5466
5827
|
} catch (err) {
|
|
5467
5828
|
warn("Result screen error", err);
|
|
5468
5829
|
stated.notify("Result screen skipped", "info");
|
|
@@ -5571,40 +5932,35 @@ function unwrapConsumed(result, ctx) {
|
|
|
5571
5932
|
return null;
|
|
5572
5933
|
}
|
|
5573
5934
|
}
|
|
5574
|
-
function
|
|
5575
|
-
const [p, ...r] =
|
|
5935
|
+
function findModelById(ctx, modelId) {
|
|
5936
|
+
const [p, ...r] = modelId.split("/");
|
|
5576
5937
|
return ctx.modelRegistry.find(p, r.join("/"));
|
|
5577
5938
|
}
|
|
5578
5939
|
function resolveModels(ctx, primary, config) {
|
|
5579
5940
|
const fallback = primary ?? ctx.model;
|
|
5580
5941
|
const available = ctx.modelRegistry.getAvailable();
|
|
5581
5942
|
let sumModel = fallback;
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
const found = ctx.modelRegistry.find(p, r.join("/"));
|
|
5586
|
-
if (found) {
|
|
5943
|
+
if (config.summaryModel) {
|
|
5944
|
+
const found = findModelById(ctx, config.summaryModel);
|
|
5945
|
+
if (found)
|
|
5587
5946
|
sumModel = found;
|
|
5588
|
-
break;
|
|
5589
|
-
}
|
|
5590
5947
|
}
|
|
5591
5948
|
if (sumModel === fallback && !fallback)
|
|
5592
5949
|
sumModel = available[0];
|
|
5593
5950
|
let segModel = sumModel;
|
|
5594
5951
|
if (config.segmentationModel) {
|
|
5595
|
-
|
|
5596
|
-
segModel = ctx.modelRegistry.find(p, r.join("/")) ?? sumModel;
|
|
5952
|
+
segModel = findModelById(ctx, config.segmentationModel) ?? sumModel;
|
|
5597
5953
|
}
|
|
5598
5954
|
return { segModel, sumModel };
|
|
5599
5955
|
}
|
|
5600
5956
|
function smartCompactExtension(pi) {
|
|
5601
|
-
const PENDING_TTL_MS =
|
|
5957
|
+
const PENDING_TTL_MS = FIVE_MINUTES_MS;
|
|
5602
5958
|
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
5603
5959
|
const isRunning = { value: false };
|
|
5604
5960
|
pi.registerCommand("smart-compact", {
|
|
5605
|
-
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
|
|
5961
|
+
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run|restore|metrics|dashboard] [note]",
|
|
5606
5962
|
getArgumentCompletions: (prefix) => {
|
|
5607
|
-
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
5963
|
+
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "restore", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
5608
5964
|
return m.length ? m : null;
|
|
5609
5965
|
},
|
|
5610
5966
|
handler: async (args, ctx) => {
|
|
@@ -5633,6 +5989,48 @@ function smartCompactExtension(pi) {
|
|
|
5633
5989
|
}
|
|
5634
5990
|
return;
|
|
5635
5991
|
}
|
|
5992
|
+
if (flags.includes("restore")) {
|
|
5993
|
+
const backups = listBackups();
|
|
5994
|
+
if (!backups.length) {
|
|
5995
|
+
ctx.ui.notify("No smart-compact backups found", "info");
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
const selected = await showRestorePicker(ctx, backups);
|
|
5999
|
+
if (!selected) {
|
|
6000
|
+
ctx.ui.notify("Cancelled", "info");
|
|
6001
|
+
return;
|
|
6002
|
+
}
|
|
6003
|
+
const content = readBackupContent(selected);
|
|
6004
|
+
if (!content) {
|
|
6005
|
+
ctx.ui.notify("Could not read backup: " + selected, "error");
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
const action = await showRestoreAction(ctx, selected);
|
|
6009
|
+
if (action === "restore") {
|
|
6010
|
+
const branch = ctx.sessionManager.getBranch();
|
|
6011
|
+
const leafId = branch.length ? branch[branch.length - 1].id : undefined;
|
|
6012
|
+
if (!leafId) {
|
|
6013
|
+
ctx.ui.notify("Cannot restore: no session leaf to fork from \u2014 showing content instead", "warning");
|
|
6014
|
+
await showBackupViewer(ctx, content, selected);
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
6017
|
+
try {
|
|
6018
|
+
const result = await ctx.fork(leafId, {
|
|
6019
|
+
withSession: async (rctx) => {
|
|
6020
|
+
await rctx.sendMessage(buildRestoreMessage(content, selected), { deliverAs: "nextTurn" });
|
|
6021
|
+
}
|
|
6022
|
+
});
|
|
6023
|
+
ctx.ui.notify(result.cancelled ? "Restore cancelled" : "Restored backup into a new session", "info");
|
|
6024
|
+
} catch (e) {
|
|
6025
|
+
warn("Restore fork failed", e);
|
|
6026
|
+
ctx.ui.notify("Restore failed (" + (e instanceof Error ? e.message : String(e)) + ") \u2014 showing content instead", "warning");
|
|
6027
|
+
await showBackupViewer(ctx, content, selected);
|
|
6028
|
+
}
|
|
6029
|
+
} else if (action === "view") {
|
|
6030
|
+
await showBackupViewer(ctx, content, selected);
|
|
6031
|
+
}
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
5636
6034
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
5637
6035
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
5638
6036
|
const profile = profileArg ?? loadConfig().profile;
|
|
@@ -5661,7 +6059,7 @@ function smartCompactExtension(pi) {
|
|
|
5661
6059
|
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning, force: true });
|
|
5662
6060
|
return;
|
|
5663
6061
|
}
|
|
5664
|
-
const { segModel, sumModel } = resolveModels(ctx, modelArg ?
|
|
6062
|
+
const { segModel, sumModel } = resolveModels(ctx, modelArg ? findModelById(ctx, modelArg) : ctx.model, loadConfig());
|
|
5665
6063
|
if (!sumModel) {
|
|
5666
6064
|
ctx.ui.notify("Could not resolve model", "error");
|
|
5667
6065
|
return;
|