pi-smart-compact 9.0.0 → 9.2.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/ARCHITECTURE.md +61 -36
- package/CHANGELOG.md +28 -0
- package/README.md +30 -5
- package/dist/app/native-continuity-bridge.d.ts.map +1 -1
- package/dist/app/pending-slot.d.ts.map +1 -1
- package/dist/app/preflight.d.ts +3 -3
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/session-run-lock.d.ts.map +1 -1
- package/dist/app/settled-auto-trigger.d.ts +20 -0
- package/dist/app/settled-auto-trigger.d.ts.map +1 -0
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/metrics.d.ts +4 -4
- package/dist/app/steps/metrics.d.ts.map +1 -1
- package/dist/app/steps/persist.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/app/steps/window.d.ts +1 -1
- package/dist/app/steps/window.d.ts.map +1 -1
- package/dist/constants.d.ts +8 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/domain/scrub.d.ts.map +1 -1
- package/dist/domain/summary-parse.d.ts.map +1 -1
- package/dist/domain/tool-semantics.d.ts +6 -9
- package/dist/domain/tool-semantics.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1216 -364
- package/dist/infra/ai-messages.d.ts +6 -0
- package/dist/infra/ai-messages.d.ts.map +1 -1
- package/dist/infra/context-graph.d.ts.map +1 -1
- package/dist/infra/fs.d.ts +2 -0
- package/dist/infra/fs.d.ts.map +1 -1
- package/dist/phases/explore.d.ts +2 -1
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts +3 -2
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/provider-eval.js +216 -33
- package/dist/provider-scenario-eval.js +279 -57
- package/dist/telemetry-report.js +216 -33
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/dashboard-format.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +2 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +2 -1
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +14 -2
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/dist/utils/state.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts +2 -0
- package/dist/utils/tokens.d.ts.map +1 -1
- package/dist/utils/type-guards.d.ts +2 -0
- package/dist/utils/type-guards.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
7
7
|
import { Type as Type2 } from "typebox";
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
-
var VERSION = "9.
|
|
10
|
+
var VERSION = "9.2.0";
|
|
11
11
|
var CHARS_PER_TOKEN = 3.8;
|
|
12
12
|
var MIN_COMPACTION_SAVING_RATIO = 0.1;
|
|
13
13
|
var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
|
|
@@ -15,6 +15,7 @@ var POST_SUMMARY_RESERVE_RATIO = 0.25;
|
|
|
15
15
|
var MAX_STATE_OPEN_LOOPS = 25;
|
|
16
16
|
var AUTO_TRIGGER_TIMEOUT_CAP_MS = 60000;
|
|
17
17
|
var AUTO_TRIGGER_MAX_LLM_CALLS = 4;
|
|
18
|
+
var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
|
|
18
19
|
var BUDGET_LIMITS = {
|
|
19
20
|
CALLS: { min: 1, max: 100 },
|
|
20
21
|
INPUT_TOKENS: { min: 1e4, max: 1e6 },
|
|
@@ -57,6 +58,7 @@ var DEFAULT_CONFIG = {
|
|
|
57
58
|
summaryThinkingLevel: "minimal",
|
|
58
59
|
segmentationThinkingLevel: "minimal",
|
|
59
60
|
autoTrigger: true,
|
|
61
|
+
autoTriggerStrategy: "native-hook",
|
|
60
62
|
autoTriggerTimeoutMs: 120000,
|
|
61
63
|
backupEnabled: true,
|
|
62
64
|
backupDir: "",
|
|
@@ -225,6 +227,7 @@ var BACKUP_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
|
225
227
|
var FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
226
228
|
var ONE_HOUR_MS = 60 * 60 * 1000;
|
|
227
229
|
var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
230
|
+
var STATE_SNAPSHOT_MAX_FILES = 64;
|
|
228
231
|
var EXTRACTION_LIMITS = {
|
|
229
232
|
MODIFIED_FILES: 120,
|
|
230
233
|
READ_FILES: 160,
|
|
@@ -232,8 +235,10 @@ var EXTRACTION_LIMITS = {
|
|
|
232
235
|
ERRORS: 80,
|
|
233
236
|
DECISIONS: 80,
|
|
234
237
|
CONSTRAINTS: 80,
|
|
238
|
+
TOPICS: 80,
|
|
235
239
|
TIMELINE: 120,
|
|
236
|
-
MEDIA_ATTACHMENTS: 40
|
|
240
|
+
MEDIA_ATTACHMENTS: 40,
|
|
241
|
+
REFERENCED_FILES: 200
|
|
237
242
|
};
|
|
238
243
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
239
244
|
var EXTRACTION_CACHE_PREFIX = "compact-extraction-";
|
|
@@ -285,6 +290,7 @@ var TRUNC = {
|
|
|
285
290
|
FINGERPRINT_SEG: 2
|
|
286
291
|
};
|
|
287
292
|
var MAX_TOOL_OUTPUT_CHARS = 800;
|
|
293
|
+
var MAX_EXPLORER_OUTPUT_CHARS = 12000;
|
|
288
294
|
var 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;
|
|
289
295
|
var ERROR_RETRY_WINDOW = 6;
|
|
290
296
|
var ERROR_RESOLVE_WINDOW = 10;
|
|
@@ -485,6 +491,51 @@ function appendLineLocked(target, line, maxBytes) {
|
|
|
485
491
|
release();
|
|
486
492
|
}
|
|
487
493
|
}
|
|
494
|
+
async function appendLineLockedAsync(target, line, maxBytes) {
|
|
495
|
+
await ensureDirAsync(path.dirname(target));
|
|
496
|
+
const payload = Buffer.from(line.endsWith(`
|
|
497
|
+
`) ? line : line + `
|
|
498
|
+
`);
|
|
499
|
+
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
|
|
500
|
+
throw new Error("maxBytes must be a positive safe integer");
|
|
501
|
+
}
|
|
502
|
+
if (maxBytes !== undefined && payload.length > maxBytes) {
|
|
503
|
+
throw new Error("Log entry exceeds retention cap for " + target);
|
|
504
|
+
}
|
|
505
|
+
const release = await acquireLock(target);
|
|
506
|
+
try {
|
|
507
|
+
let stat = null;
|
|
508
|
+
try {
|
|
509
|
+
stat = await fsp.stat(target);
|
|
510
|
+
} catch (error2) {
|
|
511
|
+
if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
|
|
512
|
+
throw error2;
|
|
513
|
+
}
|
|
514
|
+
if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
|
|
515
|
+
const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
|
|
516
|
+
const buffer = Buffer.allocUnsafe(retainedLength);
|
|
517
|
+
if (retainedLength > 0) {
|
|
518
|
+
const handle = await fsp.open(target, "r");
|
|
519
|
+
try {
|
|
520
|
+
await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
|
|
521
|
+
} finally {
|
|
522
|
+
await handle.close();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let tail = buffer.toString("utf8");
|
|
526
|
+
if (retainedLength < stat.size) {
|
|
527
|
+
const firstNewline = tail.indexOf(`
|
|
528
|
+
`);
|
|
529
|
+
tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
|
|
530
|
+
}
|
|
531
|
+
await atomicWriteFile(target, tail);
|
|
532
|
+
}
|
|
533
|
+
await fsp.appendFile(target, payload, { mode: 384 });
|
|
534
|
+
await fsp.chmod(target, 384);
|
|
535
|
+
} finally {
|
|
536
|
+
release();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
488
539
|
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
489
540
|
if (limit <= 0 || !fs.existsSync(target))
|
|
490
541
|
return [];
|
|
@@ -810,6 +861,13 @@ function getProviderCaps(provider) {
|
|
|
810
861
|
}
|
|
811
862
|
return DEFAULT_CAPS;
|
|
812
863
|
}
|
|
864
|
+
function safeContextPercent(totalTokens, contextWindow) {
|
|
865
|
+
if (!Number.isFinite(totalTokens) || !Number.isFinite(contextWindow))
|
|
866
|
+
return 0;
|
|
867
|
+
if ((totalTokens ?? 0) <= 0 || (contextWindow ?? 0) <= 0)
|
|
868
|
+
return 0;
|
|
869
|
+
return totalTokens / contextWindow * 100;
|
|
870
|
+
}
|
|
813
871
|
|
|
814
872
|
class TokenCalibrationStore {
|
|
815
873
|
maxEntries;
|
|
@@ -894,11 +952,14 @@ function makeTokenEstimator(provider, model, calibration = _fallbackCalibration)
|
|
|
894
952
|
}
|
|
895
953
|
|
|
896
954
|
// src/utils/type-guards.ts
|
|
955
|
+
function isRecord(value) {
|
|
956
|
+
return typeof value === "object" && value !== null;
|
|
957
|
+
}
|
|
897
958
|
function isTextBlock(c) {
|
|
898
|
-
return
|
|
959
|
+
return isRecord(c) && c.type === "text" && typeof c.text === "string";
|
|
899
960
|
}
|
|
900
961
|
function isToolCallBlock(c) {
|
|
901
|
-
return
|
|
962
|
+
return isRecord(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord(c.arguments);
|
|
902
963
|
}
|
|
903
964
|
function getToolCallNames(content) {
|
|
904
965
|
if (!Array.isArray(content))
|
|
@@ -1056,6 +1117,19 @@ function buildPathNeedles(filePath) {
|
|
|
1056
1117
|
}
|
|
1057
1118
|
return needles;
|
|
1058
1119
|
}
|
|
1120
|
+
function buildUniquePathNeedles(filePath, allPaths) {
|
|
1121
|
+
const normalized = allPaths.map(normalizePath);
|
|
1122
|
+
return buildPathNeedles(filePath).filter((needle) => {
|
|
1123
|
+
let owners = 0;
|
|
1124
|
+
for (const candidate of normalized) {
|
|
1125
|
+
if (candidate === needle || candidate.endsWith("/" + needle))
|
|
1126
|
+
owners++;
|
|
1127
|
+
if (owners > 1)
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
return owners === 1;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1059
1133
|
function isKnownPathReference(ref, knownPaths) {
|
|
1060
1134
|
const normalizedRef = normalizePath(ref);
|
|
1061
1135
|
return knownPaths.some((path3) => {
|
|
@@ -1102,6 +1176,152 @@ function extractToolPath(args) {
|
|
|
1102
1176
|
}
|
|
1103
1177
|
return;
|
|
1104
1178
|
}
|
|
1179
|
+
function tokenizeShell(command) {
|
|
1180
|
+
const tokens = [];
|
|
1181
|
+
let word = "";
|
|
1182
|
+
let quote = null;
|
|
1183
|
+
const flush = () => {
|
|
1184
|
+
if (word)
|
|
1185
|
+
tokens.push({ kind: "word", value: word });
|
|
1186
|
+
word = "";
|
|
1187
|
+
};
|
|
1188
|
+
for (let index = 0;index < command.length; index++) {
|
|
1189
|
+
const char = command[index];
|
|
1190
|
+
if (char === "\\" && quote !== "'" && index + 1 < command.length) {
|
|
1191
|
+
word += command[++index];
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (char === "'" || char === '"') {
|
|
1195
|
+
if (!quote)
|
|
1196
|
+
quote = char;
|
|
1197
|
+
else if (quote === char)
|
|
1198
|
+
quote = null;
|
|
1199
|
+
else
|
|
1200
|
+
word += char;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (quote) {
|
|
1204
|
+
word += char;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (/\s/.test(char)) {
|
|
1208
|
+
flush();
|
|
1209
|
+
if (char === `
|
|
1210
|
+
`)
|
|
1211
|
+
tokens.push({ kind: "separator", value: char });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (char === ">" || char === ";" || char === "|" || char === "&" && command[index + 1] === "&") {
|
|
1215
|
+
flush();
|
|
1216
|
+
if (char === ">") {
|
|
1217
|
+
const append = command[index + 1] === ">";
|
|
1218
|
+
if (append)
|
|
1219
|
+
index++;
|
|
1220
|
+
tokens.push({ kind: "redirect", value: append ? ">>" : ">" });
|
|
1221
|
+
} else {
|
|
1222
|
+
const paired = char === "|" && command[index + 1] === "|" || char === "&" && command[index + 1] === "&";
|
|
1223
|
+
if (paired)
|
|
1224
|
+
index++;
|
|
1225
|
+
tokens.push({ kind: "separator", value: paired ? char + char : char });
|
|
1226
|
+
}
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
word += char;
|
|
1230
|
+
}
|
|
1231
|
+
flush();
|
|
1232
|
+
return tokens;
|
|
1233
|
+
}
|
|
1234
|
+
function literalShellPath(token) {
|
|
1235
|
+
if (!token || token.startsWith("-") || token === "/dev/null")
|
|
1236
|
+
return;
|
|
1237
|
+
if (/[\u0000$*?\[\]{}()<>|;&]/.test(token) || /^\d+$/.test(token))
|
|
1238
|
+
return;
|
|
1239
|
+
return token;
|
|
1240
|
+
}
|
|
1241
|
+
function shellOperands(words, start) {
|
|
1242
|
+
const operands = [];
|
|
1243
|
+
let options = true;
|
|
1244
|
+
for (let index = start;index < words.length; index++) {
|
|
1245
|
+
const word = words[index];
|
|
1246
|
+
if (options && word === "--") {
|
|
1247
|
+
options = false;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (options && word.startsWith("-"))
|
|
1251
|
+
continue;
|
|
1252
|
+
const target = literalShellPath(word);
|
|
1253
|
+
if (target)
|
|
1254
|
+
operands.push(target);
|
|
1255
|
+
}
|
|
1256
|
+
return operands;
|
|
1257
|
+
}
|
|
1258
|
+
function commandFileOperations(words) {
|
|
1259
|
+
const modified = [];
|
|
1260
|
+
const deleted = [];
|
|
1261
|
+
let commandIndex = 0;
|
|
1262
|
+
while (commandIndex < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[commandIndex]))
|
|
1263
|
+
commandIndex++;
|
|
1264
|
+
while (commandIndex < words.length) {
|
|
1265
|
+
const wrapper = words[commandIndex].split("/").pop()?.toLowerCase();
|
|
1266
|
+
if (wrapper !== "env" && wrapper !== "sudo" && wrapper !== "command" && wrapper !== "nohup")
|
|
1267
|
+
break;
|
|
1268
|
+
commandIndex++;
|
|
1269
|
+
while (commandIndex < words.length && words[commandIndex].startsWith("-"))
|
|
1270
|
+
commandIndex++;
|
|
1271
|
+
}
|
|
1272
|
+
const command = words[commandIndex]?.split("/").pop()?.toLowerCase();
|
|
1273
|
+
const operands = shellOperands(words, commandIndex + 1);
|
|
1274
|
+
if (!command || !operands.length)
|
|
1275
|
+
return { modified, deleted };
|
|
1276
|
+
if (command === "rm" || command === "unlink") {
|
|
1277
|
+
deleted.push(...operands);
|
|
1278
|
+
} else if (command === "touch" || command === "tee") {
|
|
1279
|
+
modified.push(...operands);
|
|
1280
|
+
} else if (command === "cp" || command === "install") {
|
|
1281
|
+
modified.push(operands[operands.length - 1]);
|
|
1282
|
+
} else if (command === "mv") {
|
|
1283
|
+
deleted.push(...operands.slice(0, -1));
|
|
1284
|
+
modified.push(operands[operands.length - 1]);
|
|
1285
|
+
} else if (command === "sed" && words.slice(commandIndex + 1).some((word) => /^-i|^--in-place/.test(word))) {
|
|
1286
|
+
modified.push(operands[operands.length - 1]);
|
|
1287
|
+
}
|
|
1288
|
+
return { modified, deleted };
|
|
1289
|
+
}
|
|
1290
|
+
function extractShellFileOperations(args) {
|
|
1291
|
+
const record = args && typeof args === "object" ? args : null;
|
|
1292
|
+
const command = record ? COMMAND_KEYS.map((key) => record[key]).find((value) => typeof value === "string") : undefined;
|
|
1293
|
+
if (!command)
|
|
1294
|
+
return { modified: [], deleted: [] };
|
|
1295
|
+
const tokens = tokenizeShell(command);
|
|
1296
|
+
const modified = [];
|
|
1297
|
+
const deleted = [];
|
|
1298
|
+
let words = [];
|
|
1299
|
+
const flushCommand = () => {
|
|
1300
|
+
const operations = commandFileOperations(words);
|
|
1301
|
+
modified.push(...operations.modified);
|
|
1302
|
+
deleted.push(...operations.deleted);
|
|
1303
|
+
words = [];
|
|
1304
|
+
};
|
|
1305
|
+
for (let index = 0;index < tokens.length; index++) {
|
|
1306
|
+
const token = tokens[index];
|
|
1307
|
+
if (token.kind === "separator") {
|
|
1308
|
+
flushCommand();
|
|
1309
|
+
} else if (token.kind === "redirect") {
|
|
1310
|
+
const target = tokens[index + 1]?.kind === "word" ? literalShellPath(tokens[index + 1].value) : undefined;
|
|
1311
|
+
if (target)
|
|
1312
|
+
modified.push(target);
|
|
1313
|
+
if (target)
|
|
1314
|
+
index++;
|
|
1315
|
+
} else {
|
|
1316
|
+
words.push(token.value);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
flushCommand();
|
|
1320
|
+
return {
|
|
1321
|
+
modified: Array.from(new Set(modified)),
|
|
1322
|
+
deleted: Array.from(new Set(deleted.filter((file) => !modified.includes(file))))
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1105
1325
|
function normalizeToolName(name) {
|
|
1106
1326
|
if (typeof name !== "string")
|
|
1107
1327
|
return "";
|
|
@@ -1276,6 +1496,7 @@ function parseSummary(markdown) {
|
|
|
1276
1496
|
let currentHeading = "";
|
|
1277
1497
|
let currentKind = "unknown";
|
|
1278
1498
|
let bodyLines = [];
|
|
1499
|
+
let fence = null;
|
|
1279
1500
|
let started = false;
|
|
1280
1501
|
const flush = () => {
|
|
1281
1502
|
if (!started)
|
|
@@ -1289,16 +1510,31 @@ function parseSummary(markdown) {
|
|
|
1289
1510
|
sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
|
|
1290
1511
|
};
|
|
1291
1512
|
for (const line of lines) {
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1513
|
+
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
1514
|
+
if (fenceMatch) {
|
|
1515
|
+
const marker = fenceMatch[1][0];
|
|
1516
|
+
const markerLength = fenceMatch[1].length;
|
|
1517
|
+
if (!fence) {
|
|
1518
|
+
fence = { marker, length: markerLength };
|
|
1519
|
+
} else if (marker === fence.marker && markerLength >= fence.length && !fenceMatch[2].trim()) {
|
|
1520
|
+
fence = null;
|
|
1521
|
+
}
|
|
1522
|
+
if (started)
|
|
1523
|
+
bodyLines.push(line);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (!fence) {
|
|
1527
|
+
const heading = line.match(HEADING_RE);
|
|
1528
|
+
if (heading) {
|
|
1529
|
+
const kind = classifyHeading(heading[2]);
|
|
1530
|
+
if (heading[1].length <= 2 || kind !== "unknown") {
|
|
1531
|
+
flush();
|
|
1532
|
+
currentHeading = "## " + heading[2].trim();
|
|
1533
|
+
currentKind = kind;
|
|
1534
|
+
bodyLines = [];
|
|
1535
|
+
started = true;
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1302
1538
|
}
|
|
1303
1539
|
}
|
|
1304
1540
|
if (started)
|
|
@@ -1472,39 +1708,61 @@ function buildToolCallIndex(msgs) {
|
|
|
1472
1708
|
function trackFileOps(msgs, _tcIdx) {
|
|
1473
1709
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
1474
1710
|
const modMap = new Map;
|
|
1475
|
-
const
|
|
1476
|
-
const
|
|
1711
|
+
const readAt = new Map;
|
|
1712
|
+
const deletedAt = new Map;
|
|
1713
|
+
const referencedAt = new Map;
|
|
1477
1714
|
for (let i = 0;i < msgs.length; i++) {
|
|
1478
1715
|
const m = msgs[i];
|
|
1716
|
+
for (const ref of extractFileRefs((JSON.stringify(m.content) ?? "").replace(/\\[nrt]/g, " "))) {
|
|
1717
|
+
referencedAt.set(ref, i);
|
|
1718
|
+
}
|
|
1479
1719
|
if (m.role !== "toolResult" || m.isError)
|
|
1480
1720
|
continue;
|
|
1481
1721
|
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
1482
1722
|
if (!tc)
|
|
1483
1723
|
continue;
|
|
1724
|
+
const operation = classifyToolOperation(tc.arguments, tc.name);
|
|
1725
|
+
if (operation === "execute") {
|
|
1726
|
+
const resultText = extractText(m.content);
|
|
1727
|
+
if (hasCommandFailureSignal(resultText))
|
|
1728
|
+
continue;
|
|
1729
|
+
const shell = extractShellFileOperations(tc.arguments);
|
|
1730
|
+
for (const file of shell.modified) {
|
|
1731
|
+
const existing = modMap.get(file);
|
|
1732
|
+
modMap.set(file, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1733
|
+
deletedAt.delete(file);
|
|
1734
|
+
}
|
|
1735
|
+
for (const file of shell.deleted) {
|
|
1736
|
+
deletedAt.set(file, i);
|
|
1737
|
+
modMap.delete(file);
|
|
1738
|
+
readAt.delete(file);
|
|
1739
|
+
}
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1484
1742
|
const filePath = extractToolPath(tc.arguments);
|
|
1485
1743
|
if (!filePath)
|
|
1486
1744
|
continue;
|
|
1487
|
-
const operation = classifyToolOperation(tc.arguments, tc.name);
|
|
1488
1745
|
if (operation === "mutate") {
|
|
1489
1746
|
const resultText = extractText(m.content);
|
|
1490
1747
|
if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
|
|
1491
1748
|
const existing = modMap.get(filePath);
|
|
1492
1749
|
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1493
|
-
|
|
1750
|
+
deletedAt.delete(filePath);
|
|
1494
1751
|
}
|
|
1495
1752
|
} else if (operation === "delete") {
|
|
1496
|
-
|
|
1753
|
+
deletedAt.set(filePath, i);
|
|
1497
1754
|
modMap.delete(filePath);
|
|
1498
|
-
|
|
1755
|
+
readAt.delete(filePath);
|
|
1499
1756
|
} else if (operation === "read" || operation === "search" || operation === "list") {
|
|
1500
|
-
|
|
1501
|
-
|
|
1757
|
+
readAt.set(filePath, i);
|
|
1758
|
+
deletedAt.delete(filePath);
|
|
1502
1759
|
}
|
|
1503
1760
|
}
|
|
1504
1761
|
return {
|
|
1505
1762
|
modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
|
|
1506
|
-
read: [...
|
|
1507
|
-
deleted: [...
|
|
1763
|
+
read: [...readAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
|
|
1764
|
+
deleted: [...deletedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
|
|
1765
|
+
referenced: [...referencedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file)
|
|
1508
1766
|
};
|
|
1509
1767
|
}
|
|
1510
1768
|
function isBenignSearchResult(tc, result) {
|
|
@@ -1779,6 +2037,29 @@ function extractMainGoal(msgs) {
|
|
|
1779
2037
|
}
|
|
1780
2038
|
return null;
|
|
1781
2039
|
}
|
|
2040
|
+
var FOLLOWUP_COMPLETION_RE = /\b(?:done|completed?|finished|implemented|fixed|resolved|updated|added|removed|shipped|tamamland[\u0131i]|tamamlad[\u0131i]m|bitti|[\u00E7c][\u00F6o]z[\u00FCu]ld[\u00FCu])\b/iu;
|
|
2041
|
+
var NEGATED_COMPLETION_RE = /\b(?:not|isn['\u2019]?t|wasn['\u2019]?t|hen[\u00FCu]z|de[\u011Fg]il)\b.{0,20}\b(?:done|complete|finished|fixed|resolved|bitti)\b/iu;
|
|
2042
|
+
var FOLLOWUP_TOKEN_STOP = {
|
|
2043
|
+
next: true,
|
|
2044
|
+
step: true,
|
|
2045
|
+
thing: true,
|
|
2046
|
+
todo: true,
|
|
2047
|
+
action: true,
|
|
2048
|
+
item: true,
|
|
2049
|
+
follow: true,
|
|
2050
|
+
still: true,
|
|
2051
|
+
need: true,
|
|
2052
|
+
have: true,
|
|
2053
|
+
gotta: true,
|
|
2054
|
+
eklenecek: true,
|
|
2055
|
+
duzeltilecek: true,
|
|
2056
|
+
d\u{fc}zeltilecek: true,
|
|
2057
|
+
gerekiyor: true,
|
|
2058
|
+
yapalim: true,
|
|
2059
|
+
yapal\u{131}m: true,
|
|
2060
|
+
kaldi: true,
|
|
2061
|
+
kald\u{131}: true
|
|
2062
|
+
};
|
|
1782
2063
|
function extractOpenLoops(msgs, extraction) {
|
|
1783
2064
|
const loops = [];
|
|
1784
2065
|
let loopId = 0;
|
|
@@ -1859,6 +2140,29 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
1859
2140
|
});
|
|
1860
2141
|
}
|
|
1861
2142
|
}
|
|
2143
|
+
for (const loop of loops) {
|
|
2144
|
+
if (loop.type !== "follow-up" || loop.sourceIndex == null)
|
|
2145
|
+
continue;
|
|
2146
|
+
const taskTokens = (loop.summary.toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) ?? []).filter((token) => !FOLLOWUP_TOKEN_STOP[token]);
|
|
2147
|
+
if (!taskTokens.length)
|
|
2148
|
+
continue;
|
|
2149
|
+
const end = Math.min(msgs.length, loop.sourceIndex + 50);
|
|
2150
|
+
for (let index = loop.sourceIndex + 1;index < end; index++) {
|
|
2151
|
+
const message = msgs[index];
|
|
2152
|
+
if (message?.role === "user")
|
|
2153
|
+
break;
|
|
2154
|
+
if (message?.role !== "assistant")
|
|
2155
|
+
continue;
|
|
2156
|
+
const response = extractText(message.content);
|
|
2157
|
+
const normalized = response.toLowerCase();
|
|
2158
|
+
if (!FOLLOWUP_COMPLETION_RE.test(response) || NEGATED_COMPLETION_RE.test(response))
|
|
2159
|
+
continue;
|
|
2160
|
+
if (taskTokens.some((token) => normalized.includes(token))) {
|
|
2161
|
+
loop.status = "resolved";
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
1862
2166
|
return loops;
|
|
1863
2167
|
}
|
|
1864
2168
|
function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
@@ -1877,20 +2181,24 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1877
2181
|
const errors = recent(allErrors, EXTRACTION_LIMITS.ERRORS);
|
|
1878
2182
|
const decisions = recent(allDecisions, EXTRACTION_LIMITS.DECISIONS);
|
|
1879
2183
|
const constraints = recent(allConstraints, EXTRACTION_LIMITS.CONSTRAINTS);
|
|
2184
|
+
const boundedTopics = recent(topics, EXTRACTION_LIMITS.TOPICS);
|
|
1880
2185
|
const timeline = recent(allTimeline, EXTRACTION_LIMITS.TIMELINE);
|
|
1881
2186
|
const mediaAttachments = recent(allMediaAttachments, EXTRACTION_LIMITS.MEDIA_ATTACHMENTS);
|
|
2187
|
+
const allReferencedFiles = tracked.referenced;
|
|
2188
|
+
const referencedFiles = recent(allReferencedFiles, EXTRACTION_LIMITS.REFERENCED_FILES);
|
|
1882
2189
|
const overflow = {
|
|
1883
2190
|
modifiedFiles: tracked.modified.length - modifiedFiles.length,
|
|
2191
|
+
referencedFiles: allReferencedFiles.length - referencedFiles.length,
|
|
1884
2192
|
readFiles: tracked.read.length - readFiles.length,
|
|
1885
2193
|
deletedFiles: tracked.deleted.length - deletedFiles.length,
|
|
1886
2194
|
errors: allErrors.length - errors.length,
|
|
1887
2195
|
decisions: allDecisions.length - decisions.length,
|
|
1888
2196
|
constraints: allConstraints.length - constraints.length,
|
|
2197
|
+
topics: topics.length - boundedTopics.length,
|
|
1889
2198
|
timeline: allTimeline.length - timeline.length,
|
|
1890
2199
|
mediaAttachments: allMediaAttachments.length - mediaAttachments.length
|
|
1891
2200
|
};
|
|
1892
2201
|
const evidenceOverflow = Object.fromEntries(Object.entries(overflow).filter(([, count]) => count > 0));
|
|
1893
|
-
const referencedFiles = Array.from(new Set(msgs.flatMap((message) => extractFileRefs((JSON.stringify(message.content) ?? "").replace(/\\[nrt]/g, " "))))).slice(0, 200);
|
|
1894
2202
|
const mainGoal = extractMainGoal(msgs);
|
|
1895
2203
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
1896
2204
|
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
@@ -1902,7 +2210,7 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1902
2210
|
errors,
|
|
1903
2211
|
decisions,
|
|
1904
2212
|
constraints,
|
|
1905
|
-
topics,
|
|
2213
|
+
topics: boundedTopics,
|
|
1906
2214
|
timeline,
|
|
1907
2215
|
mediaAttachments,
|
|
1908
2216
|
mainGoal,
|
|
@@ -1916,6 +2224,7 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1916
2224
|
// src/utils/helpers.ts
|
|
1917
2225
|
var VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
1918
2226
|
var VALID_MODES = ["auto", "fast", "balanced", "thorough"];
|
|
2227
|
+
var VALID_AUTO_TRIGGER_STRATEGIES = ["native-hook", "settled"];
|
|
1919
2228
|
var VALID_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1920
2229
|
var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
|
|
1921
2230
|
var PROFILE_NUMERIC_BOUNDS = {
|
|
@@ -1947,6 +2256,10 @@ function validateSmartCompactConfig(sc) {
|
|
|
1947
2256
|
warn("smart-compact config: autoTrigger must be boolean, got " + typeof sc.autoTrigger);
|
|
1948
2257
|
delete sc.autoTrigger;
|
|
1949
2258
|
}
|
|
2259
|
+
if ("autoTriggerStrategy" in sc && !VALID_AUTO_TRIGGER_STRATEGIES.includes(sc.autoTriggerStrategy)) {
|
|
2260
|
+
warn("smart-compact config: autoTriggerStrategy must be native-hook|settled, got " + String(sc.autoTriggerStrategy) + ". Using default '" + DEFAULT_CONFIG.autoTriggerStrategy + "'.");
|
|
2261
|
+
delete sc.autoTriggerStrategy;
|
|
2262
|
+
}
|
|
1950
2263
|
if ("backupEnabled" in sc && typeof sc.backupEnabled !== "boolean") {
|
|
1951
2264
|
warn("smart-compact config: backupEnabled must be boolean, got " + typeof sc.backupEnabled);
|
|
1952
2265
|
delete sc.backupEnabled;
|
|
@@ -2176,39 +2489,34 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
|
|
|
2176
2489
|
return candidates;
|
|
2177
2490
|
}
|
|
2178
2491
|
function collectToolCallIds(blocks, msgIndex, out) {
|
|
2179
|
-
for (const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
out.set(n.id, msgIndex);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2492
|
+
for (const block of blocks) {
|
|
2493
|
+
if (!isRecord(block) || block.type !== "toolCall")
|
|
2494
|
+
continue;
|
|
2495
|
+
if (typeof block.id === "string")
|
|
2496
|
+
out.set(block.id, msgIndex);
|
|
2497
|
+
const args = block.arguments;
|
|
2498
|
+
if (block.name !== "multi_tool_use.parallel" || !isRecord(args) || !Array.isArray(args.tool_uses))
|
|
2499
|
+
continue;
|
|
2500
|
+
for (const nested of args.tool_uses) {
|
|
2501
|
+
if (isRecord(nested) && typeof nested.id === "string")
|
|
2502
|
+
out.set(nested.id, msgIndex);
|
|
2194
2503
|
}
|
|
2195
2504
|
}
|
|
2196
2505
|
}
|
|
2197
|
-
function
|
|
2506
|
+
function buildToolCallBoundaryIndex(msgs) {
|
|
2198
2507
|
const map = new Map;
|
|
2199
2508
|
for (let i = 0;i < msgs.length; i++) {
|
|
2200
|
-
const
|
|
2201
|
-
if (
|
|
2509
|
+
const message = msgs[i].message;
|
|
2510
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
2202
2511
|
continue;
|
|
2203
|
-
const blocks = Array.isArray(
|
|
2512
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
2204
2513
|
collectToolCallIds(blocks, i, map);
|
|
2205
2514
|
}
|
|
2206
2515
|
return map;
|
|
2207
2516
|
}
|
|
2208
|
-
function guardToolCallBoundary(msgs, keepFrom) {
|
|
2517
|
+
function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryIndex(msgs)) {
|
|
2209
2518
|
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
2210
2519
|
return keepFrom;
|
|
2211
|
-
const tcMap = toolCallIndexMap(msgs);
|
|
2212
2520
|
let adjusted = keepFrom;
|
|
2213
2521
|
let changed = true;
|
|
2214
2522
|
const MAX_ITER = msgs.length + 1;
|
|
@@ -2220,10 +2528,10 @@ function guardToolCallBoundary(msgs, keepFrom) {
|
|
|
2220
2528
|
}
|
|
2221
2529
|
changed = false;
|
|
2222
2530
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
2223
|
-
const
|
|
2224
|
-
if (
|
|
2531
|
+
const message = msgs[i].message;
|
|
2532
|
+
if (!isRecord(message) || message.role !== "toolResult")
|
|
2225
2533
|
continue;
|
|
2226
|
-
const tcId =
|
|
2534
|
+
const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
2227
2535
|
if (!tcId)
|
|
2228
2536
|
continue;
|
|
2229
2537
|
const tcIdx = tcMap.get(tcId);
|
|
@@ -2236,18 +2544,17 @@ function guardToolCallBoundary(msgs, keepFrom) {
|
|
|
2236
2544
|
}
|
|
2237
2545
|
return Math.max(0, Math.min(adjusted, msgs.length));
|
|
2238
2546
|
}
|
|
2239
|
-
function advancePastToolCallBoundary(msgs, keepFrom) {
|
|
2547
|
+
function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryIndex(msgs)) {
|
|
2240
2548
|
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
2241
2549
|
return keepFrom;
|
|
2242
|
-
const tcMap = toolCallIndexMap(msgs);
|
|
2243
2550
|
let adjusted = keepFrom;
|
|
2244
2551
|
for (let iter = 0;iter <= msgs.length; iter++) {
|
|
2245
2552
|
let next = adjusted;
|
|
2246
2553
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
2247
|
-
const
|
|
2248
|
-
if (
|
|
2554
|
+
const message = msgs[i].message;
|
|
2555
|
+
if (!isRecord(message) || message.role !== "toolResult")
|
|
2249
2556
|
continue;
|
|
2250
|
-
const tcIdx = typeof
|
|
2557
|
+
const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
|
|
2251
2558
|
if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
|
|
2252
2559
|
next = i + 1;
|
|
2253
2560
|
break;
|
|
@@ -2916,30 +3223,41 @@ function mergeFindings(target, findings) {
|
|
|
2916
3223
|
for (const finding of findings)
|
|
2917
3224
|
target.set(finding.kind, (target.get(finding.kind) ?? 0) + finding.count);
|
|
2918
3225
|
}
|
|
2919
|
-
var SECRET_KEY_NAMES =
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3226
|
+
var SECRET_KEY_NAMES = {
|
|
3227
|
+
api_key: true,
|
|
3228
|
+
apikey: true,
|
|
3229
|
+
access_token: true,
|
|
3230
|
+
auth_token: true,
|
|
3231
|
+
authorization: true,
|
|
3232
|
+
password: true,
|
|
3233
|
+
passwd: true,
|
|
3234
|
+
secret: true,
|
|
3235
|
+
secret_key: true,
|
|
3236
|
+
secret_access_key: true,
|
|
3237
|
+
client_secret: true,
|
|
3238
|
+
private_key: true,
|
|
3239
|
+
database_url: true,
|
|
3240
|
+
connection_string: true,
|
|
3241
|
+
token: true,
|
|
3242
|
+
refresh_token: true,
|
|
3243
|
+
session_token: true,
|
|
3244
|
+
credential: true,
|
|
3245
|
+
credentials: true,
|
|
3246
|
+
cookie: true,
|
|
3247
|
+
set_cookie: true,
|
|
3248
|
+
otp: true,
|
|
3249
|
+
one_time_password: true,
|
|
3250
|
+
pin: true,
|
|
3251
|
+
passcode: true
|
|
3252
|
+
};
|
|
2935
3253
|
function normalizeObjectKey(key) {
|
|
2936
3254
|
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2937
3255
|
}
|
|
2938
3256
|
function isSecretBearingKey(key) {
|
|
2939
3257
|
const normalized = normalizeObjectKey(key);
|
|
2940
|
-
if (SECRET_KEY_NAMES
|
|
3258
|
+
if (SECRET_KEY_NAMES[normalized])
|
|
2941
3259
|
return true;
|
|
2942
|
-
return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key)(?:_|$)/.test(normalized);
|
|
3260
|
+
return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key|refresh_token|session_token|one_time_password|passcode)(?:_|$)/.test(normalized);
|
|
2943
3261
|
}
|
|
2944
3262
|
|
|
2945
3263
|
class SecretScrubber {
|
|
@@ -2995,7 +3313,8 @@ class SecretScrubber {
|
|
|
2995
3313
|
const output = {};
|
|
2996
3314
|
seen.set(value2, output);
|
|
2997
3315
|
for (const [key, item] of Object.entries(value2)) {
|
|
2998
|
-
|
|
3316
|
+
const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
|
|
3317
|
+
if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
|
|
2999
3318
|
output[key] = "[REDACTED:credential]";
|
|
3000
3319
|
recordCredential();
|
|
3001
3320
|
} else {
|
|
@@ -3466,76 +3785,125 @@ function reconcileCachedErrors(errors, deltaMessages, deltaToolCalls, baseMsgCou
|
|
|
3466
3785
|
return { ...error2, retryAttempted, resolved };
|
|
3467
3786
|
});
|
|
3468
3787
|
}
|
|
3788
|
+
function boundedTail(items, limit) {
|
|
3789
|
+
const dropped = Math.max(0, items.length - limit);
|
|
3790
|
+
return { values: dropped ? items.slice(-limit) : items, dropped };
|
|
3791
|
+
}
|
|
3792
|
+
function recentUnique(items, limit) {
|
|
3793
|
+
const seen = new Set;
|
|
3794
|
+
const newestFirst = [];
|
|
3795
|
+
for (let index = items.length - 1;index >= 0; index--) {
|
|
3796
|
+
const item = items[index];
|
|
3797
|
+
if (seen.has(item))
|
|
3798
|
+
continue;
|
|
3799
|
+
seen.add(item);
|
|
3800
|
+
if (newestFirst.length < limit)
|
|
3801
|
+
newestFirst.push(item);
|
|
3802
|
+
}
|
|
3803
|
+
return {
|
|
3804
|
+
values: newestFirst.reverse(),
|
|
3805
|
+
dropped: Math.max(0, seen.size - limit)
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3469
3808
|
function mergeExtractions(base, delta, baseMsgCount, deltaMessages = [], deltaToolCalls = new Map) {
|
|
3470
|
-
const offsetErrors = delta.errors.map((
|
|
3471
|
-
const offsetDecisions = delta.decisions.map((
|
|
3472
|
-
const offsetConstraints = delta.constraints.map((
|
|
3473
|
-
const offsetTopics = delta.topics.map((
|
|
3474
|
-
...
|
|
3475
|
-
startIndex:
|
|
3476
|
-
endIndex:
|
|
3809
|
+
const offsetErrors = delta.errors.map((error2) => ({ ...error2, index: error2.index + baseMsgCount }));
|
|
3810
|
+
const offsetDecisions = delta.decisions.map((decision) => ({ ...decision, index: decision.index + baseMsgCount }));
|
|
3811
|
+
const offsetConstraints = delta.constraints.map((constraint) => ({ ...constraint, index: constraint.index + baseMsgCount }));
|
|
3812
|
+
const offsetTopics = delta.topics.map((topic) => ({
|
|
3813
|
+
...topic,
|
|
3814
|
+
startIndex: topic.startIndex + baseMsgCount,
|
|
3815
|
+
endIndex: topic.endIndex + baseMsgCount
|
|
3477
3816
|
}));
|
|
3478
|
-
const offsetTimeline = delta.timeline.map((
|
|
3479
|
-
const offsetModifiedFiles = delta.modifiedFiles.map((
|
|
3480
|
-
...
|
|
3481
|
-
lastModifiedIndex:
|
|
3817
|
+
const offsetTimeline = delta.timeline.map((event) => ({ ...event, index: event.index + baseMsgCount }));
|
|
3818
|
+
const offsetModifiedFiles = delta.modifiedFiles.map((file) => ({
|
|
3819
|
+
...file,
|
|
3820
|
+
lastModifiedIndex: file.lastModifiedIndex + baseMsgCount
|
|
3821
|
+
}));
|
|
3822
|
+
const offsetMedia = (delta.mediaAttachments ?? []).map((attachment) => ({
|
|
3823
|
+
...attachment,
|
|
3824
|
+
index: attachment.index + baseMsgCount
|
|
3482
3825
|
}));
|
|
3483
|
-
const offsetMedia = (delta.mediaAttachments ?? []).map((a) => ({ ...a, index: a.index + baseMsgCount }));
|
|
3484
3826
|
const modified = new Map(base.modifiedFiles.map((file) => [file.path, { ...file }]));
|
|
3485
3827
|
for (const file of offsetModifiedFiles) {
|
|
3486
3828
|
const previous = modified.get(file.path);
|
|
3487
|
-
modified.set(file.path, previous ? {
|
|
3829
|
+
modified.set(file.path, previous ? {
|
|
3830
|
+
...file,
|
|
3831
|
+
toolCalls: previous.toolCalls + file.toolCalls,
|
|
3832
|
+
lastModifiedIndex: Math.max(previous.lastModifiedIndex, file.lastModifiedIndex)
|
|
3833
|
+
} : file);
|
|
3488
3834
|
}
|
|
3489
3835
|
const deltaPresent = new Set([...offsetModifiedFiles.map((file) => file.path), ...delta.readFiles]);
|
|
3490
3836
|
const deltaDeleted = new Set(delta.deletedFiles);
|
|
3491
3837
|
for (const file of deltaDeleted)
|
|
3492
3838
|
modified.delete(file);
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
deletedFiles.delete(file);
|
|
3839
|
+
const modifiedFiles = boundedTail([...modified.values()].sort((a, b) => a.lastModifiedIndex - b.lastModifiedIndex), EXTRACTION_LIMITS.MODIFIED_FILES);
|
|
3840
|
+
const readFiles = recentUnique([...base.readFiles, ...delta.readFiles].filter((file) => !deltaDeleted.has(file)), EXTRACTION_LIMITS.READ_FILES);
|
|
3841
|
+
const deletedFiles = recentUnique([...base.deletedFiles, ...delta.deletedFiles].filter((file) => !deltaPresent.has(file)), EXTRACTION_LIMITS.DELETED_FILES);
|
|
3842
|
+
const referencedFiles = recentUnique([...base.referencedFiles ?? [], ...delta.referencedFiles ?? []], EXTRACTION_LIMITS.REFERENCED_FILES);
|
|
3843
|
+
const mediaAttachments = boundedTail([...base.mediaAttachments ?? [], ...offsetMedia], EXTRACTION_LIMITS.MEDIA_ATTACHMENTS);
|
|
3499
3844
|
const reconciledBaseErrors = reconcileCachedErrors(base.errors, deltaMessages, deltaToolCalls, baseMsgCount);
|
|
3500
|
-
const
|
|
3845
|
+
const errors = boundedTail([...reconciledBaseErrors, ...offsetErrors].filter((error2) => !isTransientToolDiagnostic(error2.message)), EXTRACTION_LIMITS.ERRORS);
|
|
3846
|
+
const decisions = boundedTail([...base.decisions, ...offsetDecisions], EXTRACTION_LIMITS.DECISIONS);
|
|
3847
|
+
const constraints = boundedTail([...base.constraints, ...offsetConstraints], EXTRACTION_LIMITS.CONSTRAINTS);
|
|
3848
|
+
const topics = boundedTail([...base.topics, ...offsetTopics], EXTRACTION_LIMITS.TOPICS);
|
|
3849
|
+
const timeline = boundedTail([...base.timeline, ...offsetTimeline], EXTRACTION_LIMITS.TIMELINE);
|
|
3850
|
+
const dropped = {
|
|
3851
|
+
modifiedFiles: modifiedFiles.dropped,
|
|
3852
|
+
referencedFiles: referencedFiles.dropped,
|
|
3853
|
+
readFiles: readFiles.dropped,
|
|
3854
|
+
deletedFiles: deletedFiles.dropped,
|
|
3855
|
+
errors: errors.dropped,
|
|
3856
|
+
decisions: decisions.dropped,
|
|
3857
|
+
constraints: constraints.dropped,
|
|
3858
|
+
topics: topics.dropped,
|
|
3859
|
+
timeline: timeline.dropped,
|
|
3860
|
+
mediaAttachments: mediaAttachments.dropped
|
|
3861
|
+
};
|
|
3862
|
+
const evidenceOverflow = {};
|
|
3863
|
+
for (const key of Object.keys(dropped)) {
|
|
3864
|
+
const total = (base.evidenceOverflow?.[key] ?? 0) + (delta.evidenceOverflow?.[key] ?? 0) + (dropped[key] ?? 0);
|
|
3865
|
+
if (total > 0)
|
|
3866
|
+
Object.assign(evidenceOverflow, { [key]: total });
|
|
3867
|
+
}
|
|
3501
3868
|
return {
|
|
3502
|
-
modifiedFiles:
|
|
3503
|
-
readFiles:
|
|
3504
|
-
deletedFiles:
|
|
3505
|
-
referencedFiles:
|
|
3506
|
-
mediaAttachments:
|
|
3507
|
-
errors:
|
|
3508
|
-
decisions:
|
|
3509
|
-
constraints:
|
|
3510
|
-
topics:
|
|
3511
|
-
timeline:
|
|
3869
|
+
modifiedFiles: modifiedFiles.values,
|
|
3870
|
+
readFiles: readFiles.values,
|
|
3871
|
+
deletedFiles: deletedFiles.values,
|
|
3872
|
+
referencedFiles: referencedFiles.values,
|
|
3873
|
+
mediaAttachments: mediaAttachments.values,
|
|
3874
|
+
errors: errors.values,
|
|
3875
|
+
decisions: decisions.values,
|
|
3876
|
+
constraints: constraints.values,
|
|
3877
|
+
topics: topics.values,
|
|
3878
|
+
timeline: timeline.values,
|
|
3512
3879
|
mainGoal: delta.mainGoal ?? base.mainGoal,
|
|
3513
3880
|
lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
|
|
3514
|
-
lastErrors:
|
|
3515
|
-
messageCount: baseMsgCount + delta.messageCount
|
|
3881
|
+
lastErrors: errors.values.filter((error2) => !error2.resolved).map((error2) => error2.message).slice(-3),
|
|
3882
|
+
messageCount: baseMsgCount + delta.messageCount,
|
|
3883
|
+
...Object.keys(evidenceOverflow).length ? { evidenceOverflow } : {}
|
|
3516
3884
|
};
|
|
3517
3885
|
}
|
|
3518
|
-
function appendMetricsEntry(entry) {
|
|
3886
|
+
async function appendMetricsEntry(entry) {
|
|
3519
3887
|
const logPath = metricsLogFile();
|
|
3520
|
-
|
|
3888
|
+
await appendLineLockedAsync(logPath, JSON.stringify(entry), RUNTIME_LOG_MAX_BYTES);
|
|
3521
3889
|
}
|
|
3522
|
-
function appendMetricsSnapshot(sessionId, snapshot) {
|
|
3890
|
+
async function appendMetricsSnapshot(sessionId, snapshot) {
|
|
3523
3891
|
try {
|
|
3524
|
-
appendMetricsEntry({ ts: new Date().toISOString(), sessionId, ...snapshot });
|
|
3525
|
-
} catch (
|
|
3526
|
-
warn("appendMetricsSnapshot failed",
|
|
3892
|
+
await appendMetricsEntry({ ts: new Date().toISOString(), sessionId, ...snapshot });
|
|
3893
|
+
} catch (error2) {
|
|
3894
|
+
warn("appendMetricsSnapshot failed", error2);
|
|
3527
3895
|
}
|
|
3528
3896
|
}
|
|
3529
|
-
function appendMetricsLog(sessionId, extra, services) {
|
|
3897
|
+
async function appendMetricsLog(sessionId, extra, services) {
|
|
3530
3898
|
try {
|
|
3531
|
-
appendMetricsEntry({
|
|
3899
|
+
await appendMetricsEntry({
|
|
3532
3900
|
ts: new Date().toISOString(),
|
|
3533
3901
|
sessionId,
|
|
3534
3902
|
...getMetricsSummary(services),
|
|
3535
3903
|
...extra
|
|
3536
3904
|
});
|
|
3537
|
-
} catch (
|
|
3538
|
-
warn("appendMetricsLog failed",
|
|
3905
|
+
} catch (error2) {
|
|
3906
|
+
warn("appendMetricsLog failed", error2);
|
|
3539
3907
|
}
|
|
3540
3908
|
}
|
|
3541
3909
|
function readMetricsLog(limit = 100) {
|
|
@@ -3550,8 +3918,8 @@ function readMetricsLog(limit = 100) {
|
|
|
3550
3918
|
const fd = fs4.openSync(logPath, "r");
|
|
3551
3919
|
try {
|
|
3552
3920
|
const buf = Buffer.alloc(wantBytes);
|
|
3553
|
-
fs4.readSync(fd, buf, 0, wantBytes, startPos);
|
|
3554
|
-
let text = buf.toString("utf8");
|
|
3921
|
+
const bytesRead = fs4.readSync(fd, buf, 0, wantBytes, startPos);
|
|
3922
|
+
let text = buf.subarray(0, bytesRead).toString("utf8");
|
|
3555
3923
|
if (startPos > 0) {
|
|
3556
3924
|
const nl = text.indexOf(`
|
|
3557
3925
|
`);
|
|
@@ -3821,6 +4189,8 @@ function formatRunDetails(entry, title) {
|
|
|
3821
4189
|
}
|
|
3822
4190
|
if (entry.failureKind)
|
|
3823
4191
|
lines.push("Failure kind: " + entry.failureKind);
|
|
4192
|
+
if (entry.verificationStage)
|
|
4193
|
+
lines.push("Verification gate: " + entry.verificationStage);
|
|
3824
4194
|
if (entry.extractionCacheMissReason)
|
|
3825
4195
|
lines.push("Extraction miss reason: " + entry.extractionCacheMissReason);
|
|
3826
4196
|
if (entry.fallbackReason)
|
|
@@ -4439,7 +4809,20 @@ function acquireFileLease(file, staleMs) {
|
|
|
4439
4809
|
} finally {
|
|
4440
4810
|
fs5.closeSync(fd);
|
|
4441
4811
|
}
|
|
4442
|
-
|
|
4812
|
+
const lease = { file, token };
|
|
4813
|
+
lease.heartbeat = setInterval(() => {
|
|
4814
|
+
if (readLease(file)?.token !== token) {
|
|
4815
|
+
if (lease.heartbeat)
|
|
4816
|
+
clearInterval(lease.heartbeat);
|
|
4817
|
+
lease.heartbeat = undefined;
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4820
|
+
try {
|
|
4821
|
+
fs5.utimesSync(file, new Date, new Date);
|
|
4822
|
+
} catch {}
|
|
4823
|
+
}, Math.max(1e4, Math.floor(staleMs / 3)));
|
|
4824
|
+
lease.heartbeat.unref();
|
|
4825
|
+
return lease;
|
|
4443
4826
|
} catch (error2) {
|
|
4444
4827
|
if (error2.code !== "EEXIST")
|
|
4445
4828
|
throw error2;
|
|
@@ -4450,16 +4833,29 @@ function acquireFileLease(file, staleMs) {
|
|
|
4450
4833
|
if (first)
|
|
4451
4834
|
return first;
|
|
4452
4835
|
const current = readLease(file);
|
|
4453
|
-
let
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
return null;
|
|
4459
|
-
}
|
|
4836
|
+
let observedStat;
|
|
4837
|
+
try {
|
|
4838
|
+
observedStat = fs5.statSync(file);
|
|
4839
|
+
} catch {
|
|
4840
|
+
return null;
|
|
4460
4841
|
}
|
|
4842
|
+
const observedAt = Math.max(Number(current?.createdAt ?? 0), observedStat.mtimeMs);
|
|
4461
4843
|
const age = Date.now() - observedAt;
|
|
4462
|
-
|
|
4844
|
+
const livePidCeiling = Math.max(ONE_HOUR_MS, staleMs * 4);
|
|
4845
|
+
if (age <= staleMs || current?.pid && processAlive(current.pid) && age <= livePidCeiling)
|
|
4846
|
+
return null;
|
|
4847
|
+
const latest = readLease(file);
|
|
4848
|
+
let latestStat;
|
|
4849
|
+
try {
|
|
4850
|
+
latestStat = fs5.statSync(file);
|
|
4851
|
+
} catch {
|
|
4852
|
+
return null;
|
|
4853
|
+
}
|
|
4854
|
+
const latestAt = Math.max(Number(latest?.createdAt ?? 0), latestStat.mtimeMs);
|
|
4855
|
+
const latestAge = Date.now() - latestAt;
|
|
4856
|
+
if (latestAge <= staleMs || latest?.pid && processAlive(latest.pid) && latestAge <= livePidCeiling)
|
|
4857
|
+
return null;
|
|
4858
|
+
if (current?.token ? latest?.token !== current.token : latestStat.dev !== observedStat.dev || latestStat.ino !== observedStat.ino || latestStat.size !== observedStat.size || latestStat.mtimeMs !== observedStat.mtimeMs)
|
|
4463
4859
|
return null;
|
|
4464
4860
|
try {
|
|
4465
4861
|
fs5.unlinkSync(file);
|
|
@@ -4471,6 +4867,8 @@ function acquireFileLease(file, staleMs) {
|
|
|
4471
4867
|
function releaseFileLease(lease) {
|
|
4472
4868
|
if (!lease)
|
|
4473
4869
|
return;
|
|
4870
|
+
clearInterval(lease.heartbeat);
|
|
4871
|
+
lease.heartbeat = undefined;
|
|
4474
4872
|
const current = readLease(lease.file);
|
|
4475
4873
|
if (current?.token !== lease.token)
|
|
4476
4874
|
return;
|
|
@@ -5221,7 +5619,12 @@ function planCompactionWindow(input) {
|
|
|
5221
5619
|
overflowedContext,
|
|
5222
5620
|
finalSummaryAllowanceTokens
|
|
5223
5621
|
} = input;
|
|
5224
|
-
const
|
|
5622
|
+
const tokenPrefix = new Array(messageTokens.length + 1);
|
|
5623
|
+
tokenPrefix[0] = 0;
|
|
5624
|
+
for (let index = 0;index < messageTokens.length; index++) {
|
|
5625
|
+
tokenPrefix[index + 1] = tokenPrefix[index] + messageTokens[index];
|
|
5626
|
+
}
|
|
5627
|
+
const allMessageTokens = tokenPrefix[messageTokens.length];
|
|
5225
5628
|
const messageScale = totalTokens > 0 && allMessageTokens > totalTokens ? totalTokens / allMessageTokens : 1;
|
|
5226
5629
|
const fixedContextTokens = Math.max(0, totalTokens - allMessageTokens);
|
|
5227
5630
|
const adaptiveKeepTokens = modelContextWindow ? Math.min(profileCfg.keepRecentTokens * 2, Math.max(profileCfg.keepRecentTokens, modelContextWindow * 0.04)) : profileCfg.keepRecentTokens;
|
|
@@ -5244,39 +5647,50 @@ function planCompactionWindow(input) {
|
|
|
5244
5647
|
keepFrom = i;
|
|
5245
5648
|
}
|
|
5246
5649
|
const relaxedSoftBoundaries = [];
|
|
5247
|
-
const retainedAt = (from) => Math.round(
|
|
5650
|
+
const retainedAt = (from) => Math.round((allMessageTokens - tokenPrefix[from]) * messageScale);
|
|
5651
|
+
const toolCallIndex = buildToolCallBoundaryIndex(msgs);
|
|
5248
5652
|
const effectiveRetentionCeiling = Math.max(retentionCeiling, retainedAt(keepFrom));
|
|
5249
5653
|
let hardBoundaryAdjusted = false;
|
|
5250
5654
|
const trySoftBoundary = (kind, candidate) => {
|
|
5251
5655
|
if (candidate === undefined || candidate >= keepFrom)
|
|
5252
5656
|
return;
|
|
5253
|
-
const guarded = guardToolCallBoundary(msgs, candidate);
|
|
5657
|
+
const guarded = guardToolCallBoundary(msgs, candidate, toolCallIndex);
|
|
5254
5658
|
if (retainedAt(guarded) <= effectiveRetentionCeiling) {
|
|
5255
5659
|
keepFrom = guarded;
|
|
5256
5660
|
hardBoundaryAdjusted ||= guarded !== candidate;
|
|
5257
5661
|
} else
|
|
5258
5662
|
relaxedSoftBoundaries.push(kind);
|
|
5259
5663
|
};
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5664
|
+
let userOrdinal = 0;
|
|
5665
|
+
let protectedUserIndex;
|
|
5666
|
+
for (let index = msgs.length - 1;index >= 0; index--) {
|
|
5667
|
+
const message = msgs[index].message;
|
|
5668
|
+
if (!isRecord(message) || message.role !== "user")
|
|
5669
|
+
continue;
|
|
5670
|
+
userOrdinal++;
|
|
5671
|
+
protectedUserIndex = index;
|
|
5672
|
+
if (userOrdinal === 2)
|
|
5673
|
+
break;
|
|
5674
|
+
}
|
|
5675
|
+
trySoftBoundary("recent-user-turn", protectedUserIndex);
|
|
5263
5676
|
const anchor = smartKeepBoundaryCandidates(msgs, keepFrom, branch).find((candidate) => candidate.kind === "anchor");
|
|
5264
5677
|
trySoftBoundary("anchor", anchor?.keepFrom);
|
|
5265
5678
|
const topical = smartKeepBoundaryCandidates(msgs, keepFrom).find((candidate) => candidate.kind === "topical");
|
|
5266
5679
|
trySoftBoundary("topical", topical?.keepFrom);
|
|
5267
5680
|
const boundaryBeforeHardGuard = keepFrom;
|
|
5268
|
-
const backwardBoundary = guardToolCallBoundary(msgs, keepFrom);
|
|
5269
|
-
const forwardBoundary = retainedAt(backwardBoundary) > effectiveRetentionCeiling ? advancePastToolCallBoundary(msgs, keepFrom) : keepFrom;
|
|
5681
|
+
const backwardBoundary = guardToolCallBoundary(msgs, keepFrom, toolCallIndex);
|
|
5682
|
+
const forwardBoundary = retainedAt(backwardBoundary) > effectiveRetentionCeiling ? advancePastToolCallBoundary(msgs, keepFrom, toolCallIndex) : keepFrom;
|
|
5270
5683
|
keepFrom = forwardBoundary > keepFrom && forwardBoundary < msgs.length && retainedAt(forwardBoundary) <= effectiveRetentionCeiling ? forwardBoundary : backwardBoundary;
|
|
5271
5684
|
hardBoundaryAdjusted ||= keepFrom !== boundaryBeforeHardGuard;
|
|
5272
|
-
const compactTokens = Math.round(
|
|
5685
|
+
const compactTokens = Math.round(tokenPrefix[keepFrom] * messageScale);
|
|
5273
5686
|
const retainedTokens = retainedAt(keepFrom);
|
|
5274
5687
|
const projectedAfterTokens = fixedContextTokens + retainedTokens + finalSummaryAllowance;
|
|
5275
5688
|
const projectedSavedTokens = Math.max(0, totalTokens - projectedAfterTokens);
|
|
5276
5689
|
const projectedYield = totalTokens > 0 ? projectedSavedTokens / totalTokens : 0;
|
|
5277
5690
|
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
|
|
5278
5691
|
let reason = "viable";
|
|
5279
|
-
|
|
5692
|
+
const firstKeptMessage = msgs[keepFrom]?.message;
|
|
5693
|
+
if (isRecord(firstKeptMessage) && firstKeptMessage.role === "toolResult")
|
|
5280
5694
|
reason = "unsafe-tool-boundary";
|
|
5281
5695
|
else if (keepFrom <= 0)
|
|
5282
5696
|
reason = "no-eligible-prefix";
|
|
@@ -5315,7 +5729,8 @@ function resolveCompactionWindow(rc) {
|
|
|
5315
5729
|
return null;
|
|
5316
5730
|
}
|
|
5317
5731
|
const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
|
|
5318
|
-
const
|
|
5732
|
+
const modelContextWindow = rc.ctx.model?.contextWindow;
|
|
5733
|
+
const overflowedContext = !!rc.flags.overflowRecovery || Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 && totalTokens > modelContextWindow;
|
|
5319
5734
|
const plan = planCompactionWindow({
|
|
5320
5735
|
msgs,
|
|
5321
5736
|
branch,
|
|
@@ -5339,7 +5754,7 @@ function resolveCompactionWindow(rc) {
|
|
|
5339
5754
|
if (overflowedContext && plan.relaxedSoftBoundaries.length) {
|
|
5340
5755
|
rc.notify("Context exceeds the active model window. EESV will summarize through soft recent-turn/checkpoint protections while preserving complete tool-call pairs; native fallback would resend the oversized context.", "warning");
|
|
5341
5756
|
}
|
|
5342
|
-
const contextPercent =
|
|
5757
|
+
const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
|
|
5343
5758
|
if (rc.flags.force && rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
|
|
5344
5759
|
rc.notify("Manual compaction override at " + Math.round(contextPercent) + "% (" + totalTokens.toLocaleString() + "t): compacting about " + plan.compactTokens.toLocaleString() + "t while preserving " + plan.retainedTokens.toLocaleString() + "t of recent context. Early compaction is lossy; verification remains fail-closed.", "warning");
|
|
5345
5760
|
}
|
|
@@ -5394,8 +5809,8 @@ function prepareManualPreflightContext(ctx, summaryModel, tokenCalibration) {
|
|
|
5394
5809
|
const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
|
|
5395
5810
|
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
5396
5811
|
const modelContextWindow = ctx.model?.contextWindow;
|
|
5397
|
-
const contextWindowTokens = modelContextWindow ?? 0;
|
|
5398
|
-
const contextPercent =
|
|
5812
|
+
const contextWindowTokens = Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 ? modelContextWindow : 0;
|
|
5813
|
+
const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
|
|
5399
5814
|
const toolPercent = computeToolCharPercentage(branch);
|
|
5400
5815
|
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
5401
5816
|
const estimator = makeTokenEstimator(summaryModel.provider, summaryModel.id, tokenCalibration);
|
|
@@ -5461,10 +5876,11 @@ function planManualPreflight(ctx, summaryModel, mode, tokenCalibration, config,
|
|
|
5461
5876
|
}
|
|
5462
5877
|
|
|
5463
5878
|
// src/ui/overlays.ts
|
|
5464
|
-
import
|
|
5879
|
+
import path10 from "path";
|
|
5465
5880
|
|
|
5466
5881
|
// src/utils/state.ts
|
|
5467
5882
|
import fs6 from "fs";
|
|
5883
|
+
import path9 from "path";
|
|
5468
5884
|
function getStatePath(projectId, state) {
|
|
5469
5885
|
if (!state?.scope)
|
|
5470
5886
|
return compactionStateFile(projectId);
|
|
@@ -5510,9 +5926,30 @@ function freshState(fp, data) {
|
|
|
5510
5926
|
}
|
|
5511
5927
|
return sanitizeCompactionStateEvidence(data);
|
|
5512
5928
|
}
|
|
5929
|
+
function pruneScopedStateSnapshots(target) {
|
|
5930
|
+
try {
|
|
5931
|
+
const dir = path9.dirname(target);
|
|
5932
|
+
const snapshots = fs6.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => {
|
|
5933
|
+
const file = path9.join(dir, entry.name);
|
|
5934
|
+
return { file, mtimeMs: fs6.statSync(file).mtimeMs };
|
|
5935
|
+
}).filter((entry) => entry.file !== target).sort((a, b) => b.mtimeMs - a.mtimeMs || b.file.localeCompare(a.file));
|
|
5936
|
+
for (const snapshot of snapshots.slice(Math.max(0, STATE_SNAPSHOT_MAX_FILES - 1))) {
|
|
5937
|
+
try {
|
|
5938
|
+
fs6.unlinkSync(snapshot.file);
|
|
5939
|
+
} catch (error2) {
|
|
5940
|
+
debug("state snapshot cleanup failed", error2);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
} catch (error2) {
|
|
5944
|
+
debug("state snapshot retention failed", error2);
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5513
5947
|
function saveCompactionState(projectId, state) {
|
|
5514
5948
|
try {
|
|
5515
|
-
|
|
5949
|
+
const target = getStatePath(projectId, state);
|
|
5950
|
+
writeJsonSync(target, sanitizeCompactionStateEvidence(state), true);
|
|
5951
|
+
if (state.scope)
|
|
5952
|
+
pruneScopedStateSnapshots(target);
|
|
5516
5953
|
return true;
|
|
5517
5954
|
} catch (error2) {
|
|
5518
5955
|
warn("saveCompactionState failed", error2);
|
|
@@ -5520,6 +5957,11 @@ function saveCompactionState(projectId, state) {
|
|
|
5520
5957
|
}
|
|
5521
5958
|
}
|
|
5522
5959
|
function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
5960
|
+
const snapshotProbe = scopedCompactionStateFile(scope.projectId, scope.sessionId, "__snapshot__");
|
|
5961
|
+
let availableSnapshots = new Set;
|
|
5962
|
+
try {
|
|
5963
|
+
availableSnapshots = new Set(fs6.readdirSync(path9.dirname(snapshotProbe), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name));
|
|
5964
|
+
} catch {}
|
|
5523
5965
|
const ancestry = Array.from(new Set([
|
|
5524
5966
|
...branchEntryIds2,
|
|
5525
5967
|
...scope.branchHeadId ? [scope.branchHeadId] : []
|
|
@@ -5527,6 +5969,8 @@ function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
|
5527
5969
|
const valid = (state, branchHeadId) => Boolean(state?.scope?.schemaVersion === 2 && state.scope.projectId === scope.projectId && state.scope.sessionId === scope.sessionId && typeof state.scope.branchHeadId === "string" && (!branchHeadId || state.scope.branchHeadId === branchHeadId));
|
|
5528
5970
|
for (const branchHeadId of ancestry) {
|
|
5529
5971
|
const fp = scopedCompactionStateFile(scope.projectId, scope.sessionId, branchHeadId);
|
|
5972
|
+
if (!availableSnapshots.has(path9.basename(fp)))
|
|
5973
|
+
continue;
|
|
5530
5974
|
const state = freshState(fp, readJsonSync(fp));
|
|
5531
5975
|
if (valid(state, branchHeadId))
|
|
5532
5976
|
return state;
|
|
@@ -5589,6 +6033,7 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
5589
6033
|
const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
|
|
5590
6034
|
return {
|
|
5591
6035
|
goal: extraction.mainGoal,
|
|
6036
|
+
goalKey: extraction.mainGoal ? normalizeFactKey(extraction.mainGoal) : undefined,
|
|
5592
6037
|
decisions: extraction.decisions.map((d) => ({
|
|
5593
6038
|
id: ID_PREFIX.DECISION + ++decisionId,
|
|
5594
6039
|
summary: d.summary.slice(0, TRUNC.DECISION_SUMMARY),
|
|
@@ -5662,10 +6107,13 @@ function mergeCompactionStates(previous, current) {
|
|
|
5662
6107
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
5663
6108
|
const unresolvedErrors = mergeBy(activeCurrent.unresolvedErrors, activePrevious.unresolvedErrors.filter((error2) => !resolvedKeys.has(normalizeFactKey(error2.message))), (item) => normalizeFactKey(item.message), 15).map((item, index) => ({ ...item, id: ID_PREFIX.ERROR + (index + 1) }));
|
|
5664
6109
|
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops);
|
|
5665
|
-
const
|
|
6110
|
+
const currentGoalKey = activeCurrent.goalKey ?? (activeCurrent.goal ? normalizeFactKey(activeCurrent.goal) : "");
|
|
6111
|
+
const previousGoalKey = activePrevious.goalKey ?? (activePrevious.goal ? normalizeFactKey(activePrevious.goal) : "");
|
|
6112
|
+
const oldGoal = previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey ? ["Previous goal: " + activePrevious.goal] : [];
|
|
5666
6113
|
return applyContinuityOverrides({
|
|
5667
6114
|
...activeCurrent,
|
|
5668
6115
|
goal: activeCurrent.goal ?? activePrevious.goal,
|
|
6116
|
+
goalKey: activeCurrent.goal ? currentGoalKey || undefined : (activePrevious.goalKey ?? previousGoalKey) || undefined,
|
|
5669
6117
|
decisions,
|
|
5670
6118
|
constraints,
|
|
5671
6119
|
modifiedFiles: mergeBy(activeCurrent.modifiedFiles, activePrevious.modifiedFiles.filter((file) => !currentDeleted.has(normalizeFactKey(file))), normalizeFactKey, 100),
|
|
@@ -5757,7 +6205,9 @@ function computeDelta(prev, current) {
|
|
|
5757
6205
|
]);
|
|
5758
6206
|
const resolvedErrors = prev.unresolvedErrors.filter((e) => resolvedErrorKeys.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5759
6207
|
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5760
|
-
const
|
|
6208
|
+
const previousGoalKey = prev.goalKey ?? (prev.goal ? normalizeFactKey(prev.goal) : "");
|
|
6209
|
+
const currentGoalKey = current.goalKey ?? (current.goal ? normalizeFactKey(current.goal) : "");
|
|
6210
|
+
const goalChanged = Boolean(previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey);
|
|
5761
6211
|
return {
|
|
5762
6212
|
newDecisions,
|
|
5763
6213
|
removedDecisions,
|
|
@@ -5824,7 +6274,7 @@ function ensurePinnedPaths(summary, pinned) {
|
|
|
5824
6274
|
if (!pinned.length)
|
|
5825
6275
|
return summary;
|
|
5826
6276
|
const lower = summary.toLowerCase();
|
|
5827
|
-
const missing = pinned.map((
|
|
6277
|
+
const missing = pinned.map((path10) => summaryEvidenceLine(path10, TRUNC.MESSAGE)).filter((path10) => path10 && !lower.includes(path10.toLowerCase()));
|
|
5828
6278
|
if (!missing.length)
|
|
5829
6279
|
return summary;
|
|
5830
6280
|
const parsed = parseSummary(summary);
|
|
@@ -6123,7 +6573,7 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
6123
6573
|
const f = modFiles[i];
|
|
6124
6574
|
const fc = extraction.modifiedFiles.find((e) => e.path === f);
|
|
6125
6575
|
const count = fc ? " (" + fc.toolCalls + "x)" : "";
|
|
6126
|
-
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text",
|
|
6576
|
+
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path10.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
|
|
6127
6577
|
}
|
|
6128
6578
|
if (modFiles.length > maxShow) {
|
|
6129
6579
|
c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
|
|
@@ -6660,10 +7110,13 @@ function asBranchMessage(message) {
|
|
|
6660
7110
|
function asSerializableMessages(msgs) {
|
|
6661
7111
|
return msgs;
|
|
6662
7112
|
}
|
|
7113
|
+
function scrubLlmMessages(msgs, scrubber) {
|
|
7114
|
+
return scrubber.scrubValue(msgs).value;
|
|
7115
|
+
}
|
|
6663
7116
|
|
|
6664
7117
|
// src/utils/session-log.ts
|
|
6665
7118
|
import * as fs7 from "fs";
|
|
6666
|
-
import * as
|
|
7119
|
+
import * as path11 from "path";
|
|
6667
7120
|
import { StringDecoder } from "string_decoder";
|
|
6668
7121
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
6669
7122
|
function getSessionsDir() {
|
|
@@ -6706,35 +7159,54 @@ function getMaxEntries() {
|
|
|
6706
7159
|
}
|
|
6707
7160
|
var logPathCache = new Map;
|
|
6708
7161
|
var messageMapCache = new Map;
|
|
6709
|
-
function
|
|
7162
|
+
function sessionDirectoryForCwd(cwd) {
|
|
7163
|
+
const safeCwd = path11.resolve(cwd).replace(/^[/\\]/, "").replace(/[:/\\]/g, "-");
|
|
7164
|
+
return path11.join(getSessionsDir(), "--" + safeCwd + "--");
|
|
7165
|
+
}
|
|
7166
|
+
function findLogInDirectory(directory, sessionId) {
|
|
7167
|
+
if (!fs7.existsSync(directory))
|
|
7168
|
+
return null;
|
|
7169
|
+
if (/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
|
|
7170
|
+
const exact = path11.join(directory, sessionId + ".jsonl");
|
|
7171
|
+
if (fs7.existsSync(exact))
|
|
7172
|
+
return exact;
|
|
7173
|
+
}
|
|
7174
|
+
const match = fs7.readdirSync(directory, { withFileTypes: true }).find((entry) => entry.isFile() && entry.name.endsWith("_" + sessionId + ".jsonl"));
|
|
7175
|
+
return match ? path11.join(directory, match.name) : null;
|
|
7176
|
+
}
|
|
7177
|
+
function findSessionLogFile(sessionId, cwd) {
|
|
6710
7178
|
const home2 = process.env.HOME ?? "/tmp";
|
|
6711
7179
|
const now = Date.now();
|
|
6712
|
-
const
|
|
6713
|
-
|
|
6714
|
-
|
|
7180
|
+
const directDirectory = cwd ? sessionDirectoryForCwd(cwd) : null;
|
|
7181
|
+
const cacheKey = sessionId + "\x00" + (directDirectory ?? "*");
|
|
7182
|
+
const remember = (foundPath) => {
|
|
7183
|
+
lruSet(logPathCache, cacheKey, { path: foundPath, expiresAt: now + LOG_PATH_CACHE_TTL_MS, home: home2 }, getMaxEntries());
|
|
7184
|
+
return foundPath;
|
|
6715
7185
|
};
|
|
6716
7186
|
try {
|
|
6717
|
-
const cached = lruGet(logPathCache,
|
|
7187
|
+
const cached = lruGet(logPathCache, cacheKey);
|
|
6718
7188
|
if (cached && cached.home === home2 && cached.expiresAt > now)
|
|
6719
7189
|
return cached.path;
|
|
6720
7190
|
const sessionsDir2 = getSessionsDir();
|
|
6721
7191
|
if (!fs7.existsSync(sessionsDir2))
|
|
6722
7192
|
return remember(null);
|
|
6723
|
-
|
|
6724
|
-
const
|
|
6725
|
-
|
|
6726
|
-
|
|
7193
|
+
if (directDirectory) {
|
|
7194
|
+
const direct = findLogInDirectory(directDirectory, sessionId);
|
|
7195
|
+
if (direct)
|
|
7196
|
+
return remember(direct);
|
|
7197
|
+
}
|
|
7198
|
+
for (const subdir of fs7.readdirSync(sessionsDir2, { withFileTypes: true })) {
|
|
7199
|
+
if (!subdir.isDirectory())
|
|
6727
7200
|
continue;
|
|
6728
|
-
const
|
|
6729
|
-
if (
|
|
6730
|
-
|
|
6731
|
-
const
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
return remember(path10.join(subdirPath, match));
|
|
7201
|
+
const subdirPath = path11.join(sessionsDir2, subdir.name);
|
|
7202
|
+
if (subdirPath === directDirectory)
|
|
7203
|
+
continue;
|
|
7204
|
+
const found = findLogInDirectory(subdirPath, sessionId);
|
|
7205
|
+
if (found)
|
|
7206
|
+
return remember(found);
|
|
6735
7207
|
}
|
|
6736
|
-
} catch (
|
|
6737
|
-
debug("findSessionLogFile failed",
|
|
7208
|
+
} catch (error2) {
|
|
7209
|
+
debug("findSessionLogFile failed", error2);
|
|
6738
7210
|
}
|
|
6739
7211
|
return remember(null);
|
|
6740
7212
|
}
|
|
@@ -6758,8 +7230,8 @@ function normalizeLogMessage(msg, entryTimestamp) {
|
|
|
6758
7230
|
function hasTruncatedMessages(msgs) {
|
|
6759
7231
|
return msgs.some((m) => TRUNCATE_RE.test(extractText(m.content)));
|
|
6760
7232
|
}
|
|
6761
|
-
async function readOriginalMessageMap(sessionId, wantedIds) {
|
|
6762
|
-
const logPath = findSessionLogFile(sessionId);
|
|
7233
|
+
async function readOriginalMessageMap(sessionId, wantedIds, cwd) {
|
|
7234
|
+
const logPath = findSessionLogFile(sessionId, cwd);
|
|
6763
7235
|
if (!logPath) {
|
|
6764
7236
|
debug("Session log not found for " + sessionId);
|
|
6765
7237
|
return null;
|
|
@@ -6804,9 +7276,9 @@ async function readOriginalMessageMap(sessionId, wantedIds) {
|
|
|
6804
7276
|
return null;
|
|
6805
7277
|
}
|
|
6806
7278
|
}
|
|
6807
|
-
async function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
7279
|
+
async function resolveCompactionMessages(sessionId, toCompactEntries, cwd) {
|
|
6808
7280
|
const wantedIds = new Set(toCompactEntries.flatMap((entry) => entry.id ? [entry.id] : []));
|
|
6809
|
-
const logMap = await readOriginalMessageMap(sessionId, wantedIds);
|
|
7281
|
+
const logMap = await readOriginalMessageMap(sessionId, wantedIds, cwd);
|
|
6810
7282
|
if (!logMap)
|
|
6811
7283
|
return null;
|
|
6812
7284
|
let restoredCount = 0;
|
|
@@ -6840,7 +7312,7 @@ async function recoverSessionLog(rc) {
|
|
|
6840
7312
|
return convertToLlm2([asBranchMessage(entry.message)]).map((message) => ({ entryId: entry.id, message }));
|
|
6841
7313
|
});
|
|
6842
7314
|
if (hasTruncatedMessages(resolved.map((item) => item.message))) {
|
|
6843
|
-
const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact);
|
|
7315
|
+
const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact, rc.ctx.cwd);
|
|
6844
7316
|
if (fromLog) {
|
|
6845
7317
|
resolved = fromLog;
|
|
6846
7318
|
rc.notify("Using untruncated session log (" + resolved.length + " msgs)", "info");
|
|
@@ -7028,21 +7500,26 @@ function extractWithCache(rc) {
|
|
|
7028
7500
|
const currentEntryIds = rc.toCompact.map((e) => e.id);
|
|
7029
7501
|
const selectedMessages = rc.llmMessages;
|
|
7030
7502
|
const pruning = pruneRedundant(selectedMessages);
|
|
7503
|
+
const pruningUnchanged = pruning.messages.length === selectedMessages.length && pruning.messages.every((message, index) => message === selectedMessages[index]);
|
|
7031
7504
|
const currentKeptEntryIds = pruning.keptIndices.map((i) => rc.llmEntryIds[i]).filter((id) => typeof id === "string");
|
|
7032
7505
|
if (pruning.prunedCount > 0) {
|
|
7033
7506
|
rc.notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
7034
7507
|
}
|
|
7035
|
-
|
|
7508
|
+
const scrubbedMessages = scrubLlmMessages(pruning.messages, rc.services.scrubber);
|
|
7509
|
+
pruning.messages = scrubbedMessages;
|
|
7510
|
+
rc.llmMessages = scrubbedMessages;
|
|
7036
7511
|
const pruneEnd = Date.now();
|
|
7037
7512
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
7038
7513
|
const extractionStart = pruneEnd;
|
|
7039
|
-
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
7514
|
+
const convText = rc.services.scrubber.scrubText(serializeConversation(asSerializableMessages(rc.llmMessages))).value;
|
|
7040
7515
|
const convTokens = rc.estimator.text(convText);
|
|
7041
7516
|
let preparedBackup;
|
|
7042
7517
|
if (rc.config.backupEnabled) {
|
|
7043
|
-
const unchanged = pruning.messages.length === selectedMessages.length && pruning.messages.every((message, index) => message === selectedMessages[index]);
|
|
7044
7518
|
const materializeBackup = () => {
|
|
7045
|
-
|
|
7519
|
+
if (pruningUnchanged)
|
|
7520
|
+
return convText;
|
|
7521
|
+
const safeMessages = scrubLlmMessages(selectedMessages, rc.services.scrubber);
|
|
7522
|
+
const backupText = serializeConversation(asSerializableMessages(safeMessages));
|
|
7046
7523
|
return rc.services.scrubber.scrubText(backupText).value;
|
|
7047
7524
|
};
|
|
7048
7525
|
preparedBackup = prepareConversationBackup(materializeBackup, rc.sessionId, {
|
|
@@ -7058,6 +7535,7 @@ function extractWithCache(rc) {
|
|
|
7058
7535
|
const currentFirstId = rc.toCompact[0]?.id;
|
|
7059
7536
|
const currentLastId = rc.toCompact[rc.toCompact.length - 1]?.id;
|
|
7060
7537
|
let cacheUsable = false;
|
|
7538
|
+
let cacheExact = false;
|
|
7061
7539
|
let keptCount = 0;
|
|
7062
7540
|
if (cachedExt) {
|
|
7063
7541
|
const hasNewFp = !!(cachedExt.keptEntryIdsFp && cachedExt.entryIdsFp);
|
|
@@ -7066,9 +7544,11 @@ function extractWithCache(rc) {
|
|
|
7066
7544
|
const prunedPrefixMatch = hasNewFp ? isPrefixOf(cachedExt.keptEntryIdsFp, currentKeptEntryIds) : legacyPrefixMatch(cachedExt.keptEntryIds, currentKeptEntryIds);
|
|
7067
7545
|
keptCount = hasNewFp ? cachedExt.keptEntryIdsFp?.count ?? 0 : cachedExt.keptEntryIds?.length ?? 0;
|
|
7068
7546
|
if (hasNewFp || hasLegacy) {
|
|
7069
|
-
|
|
7547
|
+
const boundedCacheShape = cachedExt.extraction.modifiedFiles.length <= EXTRACTION_LIMITS.MODIFIED_FILES && (cachedExt.extraction.referencedFiles?.length ?? 0) <= EXTRACTION_LIMITS.REFERENCED_FILES && cachedExt.extraction.readFiles.length <= EXTRACTION_LIMITS.READ_FILES && cachedExt.extraction.deletedFiles.length <= EXTRACTION_LIMITS.DELETED_FILES && cachedExt.extraction.errors.length <= EXTRACTION_LIMITS.ERRORS && cachedExt.extraction.decisions.length <= EXTRACTION_LIMITS.DECISIONS && cachedExt.extraction.constraints.length <= EXTRACTION_LIMITS.CONSTRAINTS && cachedExt.extraction.topics.length <= EXTRACTION_LIMITS.TOPICS && cachedExt.extraction.timeline.length <= EXTRACTION_LIMITS.TIMELINE && (cachedExt.extraction.mediaAttachments?.length ?? 0) <= EXTRACTION_LIMITS.MEDIA_ATTACHMENTS;
|
|
7548
|
+
cacheUsable = branchPrefixMatch && prunedPrefixMatch && boundedCacheShape && cachedExt.messageCount === keptCount && cachedExt.messageCount <= rc.llmMessages.length;
|
|
7549
|
+
cacheExact = cacheUsable && cachedExt.messageCount === rc.llmMessages.length;
|
|
7070
7550
|
if (!cacheUsable) {
|
|
7071
|
-
missReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : cachedExt.messageCount !== keptCount ? "cache-shape-mismatch" : "
|
|
7551
|
+
missReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : !boundedCacheShape ? "cache-evidence-unbounded" : cachedExt.messageCount !== keptCount ? "cache-shape-mismatch" : "cache-domain-ahead";
|
|
7072
7552
|
}
|
|
7073
7553
|
} else {
|
|
7074
7554
|
missReason = "legacy-no-kept-entryids";
|
|
@@ -7076,12 +7556,18 @@ function extractWithCache(rc) {
|
|
|
7076
7556
|
}
|
|
7077
7557
|
}
|
|
7078
7558
|
if (cacheUsable && cachedExt) {
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7559
|
+
if (cacheExact) {
|
|
7560
|
+
extraction = cachedExt.extraction;
|
|
7561
|
+
rc.notify("Phase 1 Cached: exact pruned conversation reused", "info");
|
|
7562
|
+
rc.vlog("Exact extraction cache hit \u2014 " + cachedExt.messageCount + " pruned messages");
|
|
7563
|
+
} else {
|
|
7564
|
+
const newMsgs = rc.llmMessages.slice(cachedExt.messageCount);
|
|
7565
|
+
const deltaTcIdx = buildToolCallIndex(newMsgs);
|
|
7566
|
+
const delta = extractStructured(newMsgs, rc.profileCfg, deltaTcIdx);
|
|
7567
|
+
extraction = mergeExtractions(cachedExt.extraction, delta, cachedExt.messageCount, newMsgs, deltaTcIdx);
|
|
7568
|
+
rc.notify("Phase 1 Incremental: " + cachedExt.messageCount + " cached + " + newMsgs.length + " new pruned messages", "info");
|
|
7569
|
+
rc.vlog("Incremental extraction \u2014 cached pruned messages: " + cachedExt.messageCount + ", current pruned: " + rc.llmMessages.length);
|
|
7570
|
+
}
|
|
7085
7571
|
missReason = undefined;
|
|
7086
7572
|
recordExtractionCacheHit(rc.services);
|
|
7087
7573
|
} else {
|
|
@@ -7189,25 +7675,61 @@ var EXPLORATION_TOOLS = [
|
|
|
7189
7675
|
parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
|
|
7190
7676
|
}
|
|
7191
7677
|
];
|
|
7192
|
-
function
|
|
7678
|
+
function boundedExplorationValue(value, depth = 0) {
|
|
7679
|
+
if (typeof value === "string") {
|
|
7680
|
+
return value.length > TRUNC.PREVIEW_XL ? value.slice(0, TRUNC.PREVIEW_XL) + "\u2026" : value;
|
|
7681
|
+
}
|
|
7682
|
+
if (value == null || typeof value !== "object")
|
|
7683
|
+
return value;
|
|
7684
|
+
if (depth >= 3)
|
|
7685
|
+
return "[bounded]";
|
|
7686
|
+
if (Array.isArray(value))
|
|
7687
|
+
return value.slice(0, 50).map((item) => boundedExplorationValue(item, depth + 1));
|
|
7688
|
+
return Object.fromEntries(Object.entries(value).slice(0, 16).map(([key, item]) => [key, boundedExplorationValue(item, depth + 1)]));
|
|
7689
|
+
}
|
|
7690
|
+
function serializeExplorationResult(value, scrubber) {
|
|
7691
|
+
const safe = boundedExplorationValue(scrubber.scrubValue(value).value);
|
|
7692
|
+
const serialized = JSON.stringify(safe);
|
|
7693
|
+
if (serialized.length <= MAX_EXPLORER_OUTPUT_CHARS)
|
|
7694
|
+
return serialized;
|
|
7695
|
+
let excerptChars = Math.max(0, Math.floor((MAX_EXPLORER_OUTPUT_CHARS - 160) / 2));
|
|
7696
|
+
for (;; ) {
|
|
7697
|
+
const result = JSON.stringify({
|
|
7698
|
+
truncated: true,
|
|
7699
|
+
originalChars: serialized.length,
|
|
7700
|
+
head: serialized.slice(0, excerptChars),
|
|
7701
|
+
tail: serialized.slice(-excerptChars)
|
|
7702
|
+
});
|
|
7703
|
+
if (result.length <= MAX_EXPLORER_OUTPUT_CHARS)
|
|
7704
|
+
return result;
|
|
7705
|
+
if (excerptChars === 0)
|
|
7706
|
+
return JSON.stringify({ truncated: true, originalChars: serialized.length });
|
|
7707
|
+
excerptChars = Math.max(0, excerptChars - Math.max(1, result.length - MAX_EXPLORER_OUTPUT_CHARS));
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
function executeExplorationTool(call, llmMessages, scrubber = new SecretScrubber) {
|
|
7193
7711
|
const args = call.arguments ?? {};
|
|
7194
7712
|
const boundedInteger = (value, fallback, min, max) => typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, Math.trunc(value))) : fallback;
|
|
7713
|
+
let output;
|
|
7195
7714
|
switch (call.name) {
|
|
7196
7715
|
case "get_message_range": {
|
|
7197
7716
|
const s = boundedInteger(args.start, 0, 0, llmMessages.length);
|
|
7198
7717
|
const e = boundedInteger(args.end, llmMessages.length, s, llmMessages.length);
|
|
7199
|
-
|
|
7718
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7200
7719
|
idx: s + i,
|
|
7201
7720
|
role: m?.role,
|
|
7202
7721
|
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW),
|
|
7203
7722
|
toolCalls: getToolCallNames(m?.content),
|
|
7204
7723
|
isError: m?.isError
|
|
7205
|
-
}))
|
|
7724
|
+
}));
|
|
7725
|
+
break;
|
|
7206
7726
|
}
|
|
7207
7727
|
case "search_conversation": {
|
|
7208
|
-
const q =
|
|
7209
|
-
if (!q
|
|
7210
|
-
|
|
7728
|
+
const q = typeof args.query === "string" ? args.query.toLowerCase().trim() : "";
|
|
7729
|
+
if (!q) {
|
|
7730
|
+
output = [{ error: "query must be a non-empty string" }];
|
|
7731
|
+
break;
|
|
7732
|
+
}
|
|
7211
7733
|
const matches = [];
|
|
7212
7734
|
for (let i = 0;i < llmMessages.length && matches.length < 10; i++) {
|
|
7213
7735
|
const m = llmMessages[i];
|
|
@@ -7216,68 +7738,91 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
7216
7738
|
matches.push({ idx: i, m });
|
|
7217
7739
|
continue;
|
|
7218
7740
|
}
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7741
|
+
let argumentsMatch = false;
|
|
7742
|
+
for (const tc of filterToolCalls(m?.content)) {
|
|
7743
|
+
const stack = [{ value: tc.arguments, depth: 0 }];
|
|
7744
|
+
let inspected = 0;
|
|
7745
|
+
while (stack.length && inspected++ < 64 && !argumentsMatch) {
|
|
7746
|
+
const current = stack.pop();
|
|
7747
|
+
if (typeof current.value === "string") {
|
|
7748
|
+
argumentsMatch = current.value.slice(0, 2000).toLowerCase().includes(q);
|
|
7749
|
+
} else if (current.value && typeof current.value === "object" && current.depth < 3) {
|
|
7750
|
+
const values = Array.isArray(current.value) ? current.value.slice(0, 16) : Object.values(current.value).slice(0, 16);
|
|
7751
|
+
for (const value of values)
|
|
7752
|
+
stack.push({ value, depth: current.depth + 1 });
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
if (argumentsMatch)
|
|
7756
|
+
break;
|
|
7222
7757
|
}
|
|
7758
|
+
if (argumentsMatch)
|
|
7759
|
+
matches.push({ idx: i, m });
|
|
7223
7760
|
}
|
|
7224
|
-
|
|
7761
|
+
output = matches.map(({ idx, m }) => ({
|
|
7225
7762
|
idx,
|
|
7226
7763
|
role: m?.role,
|
|
7227
7764
|
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW)
|
|
7228
|
-
}))
|
|
7765
|
+
}));
|
|
7766
|
+
break;
|
|
7229
7767
|
}
|
|
7230
7768
|
case "get_recent_user_messages": {
|
|
7231
7769
|
const count = boundedInteger(args.count, 10, 1, 50);
|
|
7232
|
-
|
|
7770
|
+
output = llmMessages.filter((m) => m?.role === "user").slice(-count).map((m) => extractText(m.content).slice(0, TRUNC.PREVIEW_XL));
|
|
7771
|
+
break;
|
|
7233
7772
|
}
|
|
7234
7773
|
case "get_context_around": {
|
|
7235
7774
|
const idx = boundedInteger(args.index, 0, 0, Math.max(0, llmMessages.length - 1));
|
|
7236
7775
|
const radius = boundedInteger(args.radius, 5, 0, 25);
|
|
7237
7776
|
const s = Math.max(0, idx - radius), e = Math.min(llmMessages.length, idx + radius + 1);
|
|
7238
|
-
|
|
7777
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7239
7778
|
idx: s + i,
|
|
7240
7779
|
role: m?.role,
|
|
7241
7780
|
text: extractText(m?.content).slice(0, TRUNC.DETAIL),
|
|
7242
7781
|
toolCalls: getToolCallNames(m?.content),
|
|
7243
7782
|
isError: m?.isError
|
|
7244
|
-
}))
|
|
7783
|
+
}));
|
|
7784
|
+
break;
|
|
7245
7785
|
}
|
|
7246
7786
|
case "get_file_changes": {
|
|
7247
|
-
const target =
|
|
7248
|
-
if (!target
|
|
7249
|
-
|
|
7787
|
+
const target = typeof args.path === "string" ? args.path.toLowerCase().trim() : "";
|
|
7788
|
+
if (!target) {
|
|
7789
|
+
output = [{ error: "path must be a non-empty string" }];
|
|
7790
|
+
break;
|
|
7791
|
+
}
|
|
7250
7792
|
const results = [];
|
|
7251
|
-
for (let i = 0;i < llmMessages.length; i++) {
|
|
7252
|
-
const
|
|
7253
|
-
for (const block of tcs) {
|
|
7793
|
+
for (let i = 0;i < llmMessages.length && results.length < TRUNC.EXPLORE_RESULTS; i++) {
|
|
7794
|
+
for (const block of filterToolCalls(llmMessages[i]?.content)) {
|
|
7254
7795
|
const a = block.arguments ?? {};
|
|
7255
|
-
const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7796
|
+
const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((value) => typeof value === "string").map((value) => value.toLowerCase());
|
|
7797
|
+
if (classifyTool(block.arguments) !== "mutates" || !fileFields.some((file) => file.includes(target)))
|
|
7798
|
+
continue;
|
|
7799
|
+
const preview = extractText(llmMessages[i]?.content).slice(0, TRUNC.PREVIEW_LONG);
|
|
7800
|
+
const surgicalKeys = ["path", "file", "filePath", "file_path", "oldText", "newText", "edits", "patch"];
|
|
7801
|
+
const argsPreview = Object.fromEntries(surgicalKeys.filter((key) => a[key] !== undefined).map((key) => [key, a[key]]));
|
|
7802
|
+
const surgical = a.oldText != null || a.newText != null || a.edits != null || a.patch != null;
|
|
7803
|
+
results.push(surgical ? { idx: i, role: "assistant", toolCall: block.name ?? "mutates", args: argsPreview, preview } : { idx: i, role: "assistant", toolCall: block.name ?? "mutates", preview });
|
|
7262
7804
|
}
|
|
7263
7805
|
}
|
|
7264
|
-
|
|
7806
|
+
output = results.length ? results : [{ info: "No edits found for: " + args.path }];
|
|
7807
|
+
break;
|
|
7265
7808
|
}
|
|
7266
7809
|
case "get_error_chain": {
|
|
7267
7810
|
const errIdx = boundedInteger(args.index, 0, 0, Math.max(0, llmMessages.length - 1));
|
|
7268
7811
|
const ctxRadius = boundedInteger(args.context_radius, 8, 0, 25);
|
|
7269
7812
|
const s = Math.max(0, errIdx - ctxRadius), e = Math.min(llmMessages.length, errIdx + ctxRadius + 1);
|
|
7270
|
-
|
|
7813
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7271
7814
|
idx: s + i,
|
|
7272
7815
|
role: m?.role,
|
|
7273
7816
|
text: extractText(m?.content).slice(0, TRUNC.PREVIEW_XL),
|
|
7274
7817
|
isError: m?.isError,
|
|
7275
7818
|
toolCalls: getToolCallNames(m?.content)
|
|
7276
|
-
}))
|
|
7819
|
+
}));
|
|
7820
|
+
break;
|
|
7277
7821
|
}
|
|
7278
7822
|
default:
|
|
7279
|
-
|
|
7823
|
+
output = { error: "Unknown tool: " + call.name };
|
|
7280
7824
|
}
|
|
7825
|
+
return serializeExplorationResult(output, scrubber);
|
|
7281
7826
|
}
|
|
7282
7827
|
function parseExplorationReport(text, llmMessages) {
|
|
7283
7828
|
let json = text.trim();
|
|
@@ -7424,7 +7969,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
7424
7969
|
probeResp
|
|
7425
7970
|
];
|
|
7426
7971
|
for (const tc of toolCalls) {
|
|
7427
|
-
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
7972
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages, svc.scrubber);
|
|
7428
7973
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
7429
7974
|
}
|
|
7430
7975
|
let rounds = 1;
|
|
@@ -7457,7 +8002,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
7457
8002
|
}
|
|
7458
8003
|
messages.push(response);
|
|
7459
8004
|
for (const tc of nextToolCalls) {
|
|
7460
|
-
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
8005
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages, svc.scrubber);
|
|
7461
8006
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
7462
8007
|
}
|
|
7463
8008
|
}
|
|
@@ -7737,7 +8282,41 @@ function fitChunkBudget(messages, maxTokens, estimator) {
|
|
|
7737
8282
|
break;
|
|
7738
8283
|
estimate = estimateChunkTokens(fitted, estimator);
|
|
7739
8284
|
}
|
|
7740
|
-
|
|
8285
|
+
if (estimate <= maxTokens)
|
|
8286
|
+
return fitted;
|
|
8287
|
+
const rendered = fitted.map(renderBatchMessage).join(`
|
|
8288
|
+
`);
|
|
8289
|
+
const marker = "[\u2026chunk evidence hard-bounded for synthesis\u2026]";
|
|
8290
|
+
const candidate = (chars) => {
|
|
8291
|
+
if (chars <= 0)
|
|
8292
|
+
return [{ role: "user", content: marker }];
|
|
8293
|
+
const head = Math.ceil(chars * 0.6);
|
|
8294
|
+
return [{
|
|
8295
|
+
role: "user",
|
|
8296
|
+
content: rendered.slice(0, head) + `
|
|
8297
|
+
` + marker + `
|
|
8298
|
+
` + rendered.slice(-(chars - head))
|
|
8299
|
+
}];
|
|
8300
|
+
};
|
|
8301
|
+
let best = candidate(0);
|
|
8302
|
+
if (estimateChunkTokens(best, estimator) > maxTokens) {
|
|
8303
|
+
if (estimateChunkTokens([], estimator) <= maxTokens)
|
|
8304
|
+
return [];
|
|
8305
|
+
throw new RangeError("Token estimator cannot represent a chunk within maxChunkTokens");
|
|
8306
|
+
}
|
|
8307
|
+
let low = 0;
|
|
8308
|
+
let high = rendered.length;
|
|
8309
|
+
while (low <= high) {
|
|
8310
|
+
const middle = Math.floor((low + high) / 2);
|
|
8311
|
+
const next = candidate(middle);
|
|
8312
|
+
if (estimateChunkTokens(next, estimator) <= maxTokens) {
|
|
8313
|
+
best = next;
|
|
8314
|
+
low = middle + 1;
|
|
8315
|
+
} else {
|
|
8316
|
+
high = middle - 1;
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
return best;
|
|
7741
8320
|
}
|
|
7742
8321
|
function extendThroughToolResults(messages, start, proposedEnd) {
|
|
7743
8322
|
const callIndexes = new Map;
|
|
@@ -7765,7 +8344,10 @@ function splitOversizedChunk(ch, maxTokens, estimator) {
|
|
|
7765
8344
|
return [ch];
|
|
7766
8345
|
if (ch.messages.length <= 1) {
|
|
7767
8346
|
const messages = fitChunkBudget(ch.messages, maxTokens, estimator);
|
|
7768
|
-
|
|
8347
|
+
const tokenEstimate = estimateChunkTokens(messages, estimator);
|
|
8348
|
+
if (tokenEstimate > maxTokens)
|
|
8349
|
+
throw new RangeError("Chunk budget postcondition failed");
|
|
8350
|
+
return [{ ...ch, tokenEstimate, messages }];
|
|
7769
8351
|
}
|
|
7770
8352
|
const parts = [];
|
|
7771
8353
|
let start = 0;
|
|
@@ -7781,11 +8363,14 @@ function splitOversizedChunk(ch, maxTokens, estimator) {
|
|
|
7781
8363
|
}
|
|
7782
8364
|
const end = extendThroughToolResults(ch.messages, start, proposedEnd);
|
|
7783
8365
|
const messages = fitChunkBudget(ch.messages.slice(start, end), maxTokens, estimator);
|
|
8366
|
+
const tokenEstimate = estimateChunkTokens(messages, estimator);
|
|
8367
|
+
if (tokenEstimate > maxTokens)
|
|
8368
|
+
throw new RangeError("Chunk budget postcondition failed");
|
|
7784
8369
|
parts.push({
|
|
7785
8370
|
...ch,
|
|
7786
8371
|
startIndex: ch.startIndex + start,
|
|
7787
8372
|
endIndex: ch.startIndex + end - 1,
|
|
7788
|
-
tokenEstimate
|
|
8373
|
+
tokenEstimate,
|
|
7789
8374
|
messages
|
|
7790
8375
|
});
|
|
7791
8376
|
start = end;
|
|
@@ -8361,24 +8946,24 @@ async function summarizeConversation(rc) {
|
|
|
8361
8946
|
generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
|
|
8362
8947
|
}
|
|
8363
8948
|
let completed = totalBatches - batchCallLimit;
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8949
|
+
let nextBatch = 0;
|
|
8950
|
+
let budgetStopped = false;
|
|
8951
|
+
const runWorker = async () => {
|
|
8952
|
+
while (true) {
|
|
8953
|
+
const idx = nextBatch++;
|
|
8954
|
+
if (idx >= batchCallLimit)
|
|
8955
|
+
return;
|
|
8956
|
+
if (budgetStopped || rc.services.budget.reason()) {
|
|
8957
|
+
budgetStopped = true;
|
|
8958
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8959
|
+
} else {
|
|
8960
|
+
try {
|
|
8961
|
+
const batch = batches[idx];
|
|
8962
|
+
results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
|
|
8963
|
+
} catch (err) {
|
|
8964
|
+
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
8965
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8966
|
+
}
|
|
8382
8967
|
}
|
|
8383
8968
|
completed++;
|
|
8384
8969
|
showProgressOverlay(rc.ctx, {
|
|
@@ -8392,8 +8977,14 @@ async function summarizeConversation(rc) {
|
|
|
8392
8977
|
totalBatches,
|
|
8393
8978
|
currentBatch: completed
|
|
8394
8979
|
});
|
|
8395
|
-
}
|
|
8396
|
-
|
|
8980
|
+
}
|
|
8981
|
+
};
|
|
8982
|
+
const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
|
|
8983
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
8984
|
+
if (budgetStopped) {
|
|
8985
|
+
rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
|
|
8986
|
+
cacheable = false;
|
|
8987
|
+
generationFallbacks.push("synthesis budget exhausted during batch pool");
|
|
8397
8988
|
}
|
|
8398
8989
|
for (const r of results)
|
|
8399
8990
|
if (r)
|
|
@@ -8483,29 +9074,56 @@ function classifyOutcomeClaim(claim) {
|
|
|
8483
9074
|
return "file";
|
|
8484
9075
|
return "generic";
|
|
8485
9076
|
}
|
|
8486
|
-
|
|
9077
|
+
var successfulToolEvidenceCache = new WeakMap;
|
|
9078
|
+
function successfulToolEvidence(messages) {
|
|
9079
|
+
const cached = successfulToolEvidenceCache.get(messages);
|
|
9080
|
+
if (cached)
|
|
9081
|
+
return cached;
|
|
9082
|
+
const toolCalls = buildToolCallIndex(messages);
|
|
9083
|
+
const evidence = [];
|
|
9084
|
+
for (const message of messages) {
|
|
9085
|
+
if (message.role !== "toolResult" || message.isError)
|
|
9086
|
+
continue;
|
|
9087
|
+
const call = toolCalls.get(message.toolCallId ?? "");
|
|
9088
|
+
if (!call)
|
|
9089
|
+
continue;
|
|
9090
|
+
const result = extractText(message.content).slice(0, 8000);
|
|
9091
|
+
if (!result.trim() || LIKELY_ERROR_RE.test(result))
|
|
9092
|
+
continue;
|
|
9093
|
+
const command = [call.arguments.command, call.arguments.cmd, call.arguments.script].find((value) => typeof value === "string") ?? "";
|
|
9094
|
+
evidence.push({
|
|
9095
|
+
name: normalizeToolName(call.name),
|
|
9096
|
+
operation: classifyToolOperation(call.arguments, call.name),
|
|
9097
|
+
command,
|
|
9098
|
+
path: extractToolPath(call.arguments),
|
|
9099
|
+
result
|
|
9100
|
+
});
|
|
9101
|
+
}
|
|
9102
|
+
successfulToolEvidenceCache.set(messages, evidence);
|
|
9103
|
+
return evidence;
|
|
9104
|
+
}
|
|
9105
|
+
function successfulToolSupportsClaim(claim, tools, extraction) {
|
|
8487
9106
|
const shape = semanticShape(claim);
|
|
8488
9107
|
const category = classifyOutcomeClaim(claim);
|
|
8489
9108
|
if (category === "error" && extraction.errors.some((error2) => error2.resolved && hasSemanticEvidence(claim, error2.message)))
|
|
8490
9109
|
return true;
|
|
8491
9110
|
if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
|
|
8492
9111
|
return true;
|
|
8493
|
-
for (const
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
if (!bounded.trim() || LIKELY_ERROR_RE.test(bounded))
|
|
9112
|
+
for (const tool of tools) {
|
|
9113
|
+
const operationText = tool.name + " " + tool.command;
|
|
9114
|
+
const operationSupports = category === "test" ? /\b(?:test|tests|pytest|jest|vitest|mocha|rspec)\b/i.test(operationText) : category === "build" ? /\b(?:build|compile|typecheck|tsc|check)\b/i.test(operationText) : category === "release" ? /\b(?:deploy|publish|release)\b/i.test(operationText) : category === "file" ? tool.operation === "mutate" || tool.operation === "delete" : category === "error" ? tool.operation === "execute" || tool.operation === "mutate" || tool.operation === "delete" : tool.operation !== "read" && tool.operation !== "search" && tool.operation !== "list";
|
|
9115
|
+
if (!operationSupports)
|
|
8498
9116
|
continue;
|
|
8499
|
-
if (hasSemanticEvidence(claim,
|
|
9117
|
+
if (hasSemanticEvidence(claim, tool.result))
|
|
8500
9118
|
return true;
|
|
8501
|
-
const lower =
|
|
9119
|
+
const lower = tool.result.toLowerCase();
|
|
8502
9120
|
if (category === "test" && /\b\d+\s+(?:tests?\s+)?pass(?:ed)?\b/.test(lower) && !/\b(?:fail(?:ed|ures?)?|errors?)\s*[:=]?\s*[1-9]\d*\b/.test(lower))
|
|
8503
9121
|
return true;
|
|
8504
|
-
if (category === "build" && /\b(?:
|
|
9122
|
+
if (category === "build" && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
|
|
8505
9123
|
return true;
|
|
8506
|
-
if (category === "release" && /\b(?:
|
|
9124
|
+
if (category === "release" && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
|
|
8507
9125
|
return true;
|
|
8508
|
-
if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim,
|
|
9126
|
+
if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, tool.result))
|
|
8509
9127
|
return true;
|
|
8510
9128
|
}
|
|
8511
9129
|
return false;
|
|
@@ -8563,12 +9181,14 @@ class VerificationGateError extends Error {
|
|
|
8563
9181
|
score;
|
|
8564
9182
|
initialScore;
|
|
8565
9183
|
gapKinds;
|
|
9184
|
+
stage;
|
|
8566
9185
|
gapCount;
|
|
8567
|
-
constructor(result, initialScore) {
|
|
9186
|
+
constructor(result, initialScore, stage) {
|
|
8568
9187
|
super(verificationFailureMessage(result) ?? "Verification gate rejected summary");
|
|
8569
9188
|
this.name = "VerificationGateError";
|
|
8570
9189
|
this.score = result.score;
|
|
8571
9190
|
this.initialScore = initialScore;
|
|
9191
|
+
this.stage = stage;
|
|
8572
9192
|
this.gapKinds = Array.from(new Set(result.gaps.map((gap) => gap.kind)));
|
|
8573
9193
|
this.gapCount = result.gaps.length;
|
|
8574
9194
|
}
|
|
@@ -8653,9 +9273,15 @@ function stemToken(token) {
|
|
|
8653
9273
|
function semanticTokens(text) {
|
|
8654
9274
|
return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
|
|
8655
9275
|
}
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
9276
|
+
var semanticShapeCache = new Map;
|
|
9277
|
+
var semanticFragmentCache = new Map;
|
|
9278
|
+
function semanticFragments(text) {
|
|
9279
|
+
const cached = lruGet(semanticFragmentCache, text);
|
|
9280
|
+
if (cached)
|
|
9281
|
+
return cached;
|
|
9282
|
+
const fragments = Array.from(new Set(text.split(/\r?\n/).flatMap((line) => [line, ...line.split(/[.;]/)]).map((part) => part.replace(/^\s*[-*\d.)]+\s*/, "").trim()).filter(Boolean))).map(semanticTokens);
|
|
9283
|
+
lruSet(semanticFragmentCache, text, fragments, 256);
|
|
9284
|
+
return fragments;
|
|
8659
9285
|
}
|
|
8660
9286
|
function hasNearbyMarker(tokens, anchor, markers) {
|
|
8661
9287
|
return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
|
|
@@ -8679,20 +9305,24 @@ function hasEffectiveTargetNegation(tokens, anchor) {
|
|
|
8679
9305
|
});
|
|
8680
9306
|
}
|
|
8681
9307
|
function semanticShape(source) {
|
|
9308
|
+
const cached = lruGet(semanticShapeCache, source);
|
|
9309
|
+
if (cached)
|
|
9310
|
+
return cached;
|
|
8682
9311
|
const sourceTokens = semanticTokens(source);
|
|
8683
9312
|
const concepts = Array.from(new Set(sourceTokens.filter((token) => !/^\d+$/.test(token) && !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
|
|
8684
9313
|
const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
|
|
8685
9314
|
const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
|
|
8686
9315
|
const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
|
|
8687
|
-
|
|
9316
|
+
const shape = { sourceTokens, concepts, anchor, negative, conditional };
|
|
9317
|
+
lruSet(semanticShapeCache, source, shape, 512);
|
|
9318
|
+
return shape;
|
|
8688
9319
|
}
|
|
8689
9320
|
function hasSemanticEvidence(source, target) {
|
|
8690
9321
|
const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
|
|
8691
9322
|
if (!concepts.length)
|
|
8692
9323
|
return true;
|
|
8693
9324
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
8694
|
-
return
|
|
8695
|
-
const tokens = semanticTokens(fragment);
|
|
9325
|
+
return semanticFragments(target).some((tokens) => {
|
|
8696
9326
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
8697
9327
|
if (overlap < required)
|
|
8698
9328
|
return false;
|
|
@@ -8714,8 +9344,7 @@ function hasSemanticContradiction(source, target) {
|
|
|
8714
9344
|
if (!anchor)
|
|
8715
9345
|
return false;
|
|
8716
9346
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
8717
|
-
return
|
|
8718
|
-
const tokens = semanticTokens(fragment);
|
|
9347
|
+
return semanticFragments(target).some((tokens) => {
|
|
8719
9348
|
if (!tokens.includes(anchor))
|
|
8720
9349
|
return false;
|
|
8721
9350
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
@@ -8906,16 +9535,18 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
8906
9535
|
gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
|
|
8907
9536
|
score -= 12;
|
|
8908
9537
|
}
|
|
9538
|
+
const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
|
|
8909
9539
|
for (const file of extraction.modifiedFiles) {
|
|
8910
|
-
const
|
|
8911
|
-
if (!
|
|
9540
|
+
const uniqueNeedles = buildUniquePathNeedles(file.path, modifiedPaths);
|
|
9541
|
+
if (!uniqueNeedles.some((needle) => doneRefs.has(normalizePath(needle))))
|
|
8912
9542
|
continue;
|
|
8913
9543
|
const unresolved = unresolvedEvidence.find((error2) => {
|
|
8914
9544
|
const firstLine = error2.message.split(/\r?\n/, 1)[0] ?? "";
|
|
8915
|
-
|
|
9545
|
+
const errorRefs = extractFileRefs(firstLine).map(normalizePath);
|
|
9546
|
+
return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
|
|
8916
9547
|
});
|
|
8917
9548
|
if (unresolved) {
|
|
8918
|
-
gaps.push({ kind: "inconsistency", detail:
|
|
9549
|
+
gaps.push({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
|
|
8919
9550
|
score -= 5;
|
|
8920
9551
|
}
|
|
8921
9552
|
}
|
|
@@ -8937,8 +9568,9 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
8937
9568
|
score -= 5;
|
|
8938
9569
|
}
|
|
8939
9570
|
if (evidence.sourceMessages) {
|
|
9571
|
+
const tools = successfulToolEvidence(evidence.sourceMessages);
|
|
8940
9572
|
for (const claim of outcomeClaims(summary)) {
|
|
8941
|
-
if (!successfulToolSupportsClaim(claim,
|
|
9573
|
+
if (!successfulToolSupportsClaim(claim, tools, extraction)) {
|
|
8942
9574
|
gaps.push({ kind: "unsupported-claim", claim });
|
|
8943
9575
|
score -= 20;
|
|
8944
9576
|
}
|
|
@@ -9050,6 +9682,28 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
|
|
|
9050
9682
|
}
|
|
9051
9683
|
return renderSummary(canonical, { canonicalHeadings: true });
|
|
9052
9684
|
}
|
|
9685
|
+
function hasUnclosedMarkdownFence(markdown) {
|
|
9686
|
+
let open = null;
|
|
9687
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
9688
|
+
const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
9689
|
+
if (!match)
|
|
9690
|
+
continue;
|
|
9691
|
+
const marker = match[1][0];
|
|
9692
|
+
if (!open) {
|
|
9693
|
+
open = { marker, length: match[1].length };
|
|
9694
|
+
} else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
|
|
9695
|
+
open = null;
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
return open !== null;
|
|
9699
|
+
}
|
|
9700
|
+
function patchResponseIsTruncated(patched, stopReason) {
|
|
9701
|
+
const reason = String(stopReason ?? "");
|
|
9702
|
+
return /(?:length|truncat|max(?:imum)?[_ -]?(?:output[_ -]?)?tokens?|token[_ -]?limit)/i.test(reason) || /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
|
|
9703
|
+
}
|
|
9704
|
+
function sectionIdentity(section) {
|
|
9705
|
+
return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
|
|
9706
|
+
}
|
|
9053
9707
|
async function patchSummary(summary, gaps, model, auth, signal, services) {
|
|
9054
9708
|
const patchPrompt = `Correct every verification finding below WITHOUT restructuring the summary. Add missing evidence, remove fabricated references, and rewrite contradictory claims so they preserve the source constraint/decision polarity. Do not add a Verification Note.
|
|
9055
9709
|
|
|
@@ -9069,7 +9723,13 @@ Return the COMPLETE corrected summary in the same format.`;
|
|
|
9069
9723
|
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
|
|
9070
9724
|
const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
|
|
9071
9725
|
`).trim();
|
|
9072
|
-
|
|
9726
|
+
if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
|
|
9727
|
+
return summary;
|
|
9728
|
+
const originalSections = parseSummary(summary).sections;
|
|
9729
|
+
const patchedSections = parseSummary(patched).sections;
|
|
9730
|
+
const patchedBodies = new Map(patchedSections.map((section) => [sectionIdentity(section), section.body.trim()]));
|
|
9731
|
+
const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
|
|
9732
|
+
return preserved ? patched : summary;
|
|
9073
9733
|
} catch (error2) {
|
|
9074
9734
|
debug("patchSummary LLM failed", error2);
|
|
9075
9735
|
return summary;
|
|
@@ -9160,7 +9820,7 @@ async function verifyAndPatch(rc) {
|
|
|
9160
9820
|
}
|
|
9161
9821
|
const failure = verificationFailureMessage(verification);
|
|
9162
9822
|
if (failure)
|
|
9163
|
-
throw new VerificationGateError(verification, initialScore);
|
|
9823
|
+
throw new VerificationGateError(verification, initialScore, "post-synthesis");
|
|
9164
9824
|
showProgressOverlay(rc.ctx, {
|
|
9165
9825
|
phase: 4,
|
|
9166
9826
|
phaseName: "Verify",
|
|
@@ -9186,7 +9846,7 @@ async function verifyAndPatch(rc) {
|
|
|
9186
9846
|
|
|
9187
9847
|
// src/app/steps/state.ts
|
|
9188
9848
|
import fs8 from "fs";
|
|
9189
|
-
import
|
|
9849
|
+
import path12 from "path";
|
|
9190
9850
|
|
|
9191
9851
|
// src/domain/yield-gate.ts
|
|
9192
9852
|
class YieldGateError extends Error {
|
|
@@ -9273,7 +9933,7 @@ function buildState(rc) {
|
|
|
9273
9933
|
currentState.factOverrides = prevState?.factOverrides ?? [];
|
|
9274
9934
|
let compactionState = mergeCompactionStates(prevState, currentState);
|
|
9275
9935
|
compactionState.deletedFiles = compactionState.deletedFiles.filter((file) => {
|
|
9276
|
-
const candidate =
|
|
9936
|
+
const candidate = path12.isAbsolute(file) ? file : path12.resolve(rc.ctx.cwd, file);
|
|
9277
9937
|
return !fs8.existsSync(candidate);
|
|
9278
9938
|
});
|
|
9279
9939
|
if (preserve.length > 0) {
|
|
@@ -9317,7 +9977,7 @@ function buildState(rc) {
|
|
|
9317
9977
|
};
|
|
9318
9978
|
const failure = verificationFailureMessage(postVerification);
|
|
9319
9979
|
if (failure)
|
|
9320
|
-
throw new VerificationGateError(postVerification, postInitialScore);
|
|
9980
|
+
throw new VerificationGateError(postVerification, postInitialScore, "post-state");
|
|
9321
9981
|
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
9322
9982
|
const detRead = extraction.readFiles;
|
|
9323
9983
|
const yieldEstimate = verifyCompactionYield(rc.totalTokens, rc.estimator.text(summary), rc.compactionPlan);
|
|
@@ -9369,7 +10029,7 @@ function buildState(rc) {
|
|
|
9369
10029
|
// src/infra/context-graph.ts
|
|
9370
10030
|
import { createHash as createHash3 } from "crypto";
|
|
9371
10031
|
import fs9 from "fs";
|
|
9372
|
-
import
|
|
10032
|
+
import path13 from "path";
|
|
9373
10033
|
import { createRequire } from "module";
|
|
9374
10034
|
var require2 = createRequire(import.meta.url);
|
|
9375
10035
|
var MAX_PROJECT_NODES = 2000;
|
|
@@ -9377,7 +10037,27 @@ var MAX_MANUAL_NODES = 500;
|
|
|
9377
10037
|
var MAX_SESSION_NODES = 256;
|
|
9378
10038
|
var MAX_QUERY_CANDIDATES = 80;
|
|
9379
10039
|
var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
9380
|
-
var CONTEXT_GRAPH_SCHEMA_VERSION =
|
|
10040
|
+
var CONTEXT_GRAPH_SCHEMA_VERSION = 2;
|
|
10041
|
+
function bunSqliteAdapter(db) {
|
|
10042
|
+
return {
|
|
10043
|
+
exec: (sql) => db.exec(sql),
|
|
10044
|
+
query: (sql) => db.query(sql),
|
|
10045
|
+
transaction: (fn) => (...args) => {
|
|
10046
|
+
db.exec("BEGIN IMMEDIATE");
|
|
10047
|
+
try {
|
|
10048
|
+
const result = fn(...args);
|
|
10049
|
+
db.exec("COMMIT");
|
|
10050
|
+
return result;
|
|
10051
|
+
} catch (error2) {
|
|
10052
|
+
try {
|
|
10053
|
+
db.exec("ROLLBACK");
|
|
10054
|
+
} catch {}
|
|
10055
|
+
throw error2;
|
|
10056
|
+
}
|
|
10057
|
+
},
|
|
10058
|
+
close: () => db.close()
|
|
10059
|
+
};
|
|
10060
|
+
}
|
|
9381
10061
|
function nodeSqliteAdapter(db) {
|
|
9382
10062
|
return {
|
|
9383
10063
|
exec: (sql) => db.exec(sql),
|
|
@@ -9400,11 +10080,11 @@ function nodeSqliteAdapter(db) {
|
|
|
9400
10080
|
}
|
|
9401
10081
|
function openDatabase() {
|
|
9402
10082
|
const fp = contextGraphFile();
|
|
9403
|
-
ensureDir(
|
|
10083
|
+
ensureDir(path13.dirname(fp));
|
|
9404
10084
|
let db;
|
|
9405
10085
|
if ("bun" in process.versions) {
|
|
9406
10086
|
const { Database } = require2("bun:sqlite");
|
|
9407
|
-
db = new Database(fp);
|
|
10087
|
+
db = bunSqliteAdapter(new Database(fp));
|
|
9408
10088
|
} else {
|
|
9409
10089
|
const { DatabaseSync } = require2("node:sqlite");
|
|
9410
10090
|
db = nodeSqliteAdapter(new DatabaseSync(fp));
|
|
@@ -9432,6 +10112,8 @@ function openDatabase() {
|
|
|
9432
10112
|
);
|
|
9433
10113
|
CREATE INDEX IF NOT EXISTS context_nodes_project_status
|
|
9434
10114
|
ON context_nodes(project_id, status, updated_at DESC);
|
|
10115
|
+
CREATE INDEX IF NOT EXISTS context_nodes_lineage
|
|
10116
|
+
ON context_nodes(project_id, session_id, source, branch_head_id, kind, fact_key, updated_at DESC);
|
|
9435
10117
|
CREATE TABLE IF NOT EXISTS context_edges (
|
|
9436
10118
|
project_id TEXT NOT NULL,
|
|
9437
10119
|
from_id TEXT NOT NULL REFERENCES context_nodes(id) ON DELETE CASCADE,
|
|
@@ -9448,7 +10130,8 @@ function openDatabase() {
|
|
|
9448
10130
|
);
|
|
9449
10131
|
`);
|
|
9450
10132
|
const version = db.query("PRAGMA user_version").get();
|
|
9451
|
-
|
|
10133
|
+
const schemaVersion = Number(version?.user_version ?? 0);
|
|
10134
|
+
if (schemaVersion < 1) {
|
|
9452
10135
|
db.transaction(() => {
|
|
9453
10136
|
db.exec(`
|
|
9454
10137
|
DROP INDEX IF EXISTS context_nodes_fact;
|
|
@@ -9457,6 +10140,17 @@ function openDatabase() {
|
|
|
9457
10140
|
DELETE FROM context_nodes WHERE source = 'compaction';
|
|
9458
10141
|
CREATE UNIQUE INDEX context_nodes_fact
|
|
9459
10142
|
ON context_nodes(project_id, session_id, kind, fact_key, COALESCE(branch_head_id, ''));
|
|
10143
|
+
PRAGMA user_version = 1;
|
|
10144
|
+
`);
|
|
10145
|
+
})();
|
|
10146
|
+
}
|
|
10147
|
+
if (schemaVersion < 2) {
|
|
10148
|
+
db.transaction(() => {
|
|
10149
|
+
db.exec(`
|
|
10150
|
+
DELETE FROM context_nodes_fts;
|
|
10151
|
+
INSERT INTO context_nodes_fts(rowid, node_id, title, content, kind)
|
|
10152
|
+
SELECT rowid, id, title, content, kind FROM context_nodes
|
|
10153
|
+
WHERE status = 'active' AND kind NOT IN ('project', 'session');
|
|
9460
10154
|
PRAGMA user_version = ${CONTEXT_GRAPH_SCHEMA_VERSION};
|
|
9461
10155
|
`);
|
|
9462
10156
|
})();
|
|
@@ -9469,10 +10163,19 @@ function stableId(...parts) {
|
|
|
9469
10163
|
function factKey(text) {
|
|
9470
10164
|
return normalizeFactKey(text) || text.trim().toLowerCase();
|
|
9471
10165
|
}
|
|
10166
|
+
function removeFtsNode(db, nodeId) {
|
|
10167
|
+
db.query(`
|
|
10168
|
+
DELETE FROM context_nodes_fts
|
|
10169
|
+
WHERE rowid = (SELECT rowid FROM context_nodes WHERE id = ?)
|
|
10170
|
+
`).run(nodeId);
|
|
10171
|
+
}
|
|
9472
10172
|
function syncFts(db, node) {
|
|
9473
|
-
db.query("
|
|
10173
|
+
const row = db.query("SELECT rowid FROM context_nodes WHERE id = ?").get(node.id);
|
|
10174
|
+
if (!row)
|
|
10175
|
+
return;
|
|
10176
|
+
db.query("DELETE FROM context_nodes_fts WHERE rowid = ?").run(row.rowid);
|
|
9474
10177
|
if (node.status === "active" && node.kind !== "project" && node.kind !== "session") {
|
|
9475
|
-
db.query("INSERT INTO context_nodes_fts(node_id, title, content, kind) VALUES (?, ?, ?, ?)").run(node.id, node.title, node.content, node.kind);
|
|
10178
|
+
db.query("INSERT INTO context_nodes_fts(rowid, node_id, title, content, kind) VALUES (?, ?, ?, ?, ?)").run(row.rowid, node.id, node.title, node.content, node.kind);
|
|
9476
10179
|
}
|
|
9477
10180
|
}
|
|
9478
10181
|
function upsertNode(db, node, refresh = false) {
|
|
@@ -9541,13 +10244,22 @@ function branchLineage(scope) {
|
|
|
9541
10244
|
return Array.from(new Set([...scope.branchEntryIds ?? [], scope.branchHeadId].filter((id) => typeof id === "string" && id.length > 0)));
|
|
9542
10245
|
}
|
|
9543
10246
|
function lineageFactRows(db, scope, kind, key) {
|
|
9544
|
-
const lineage =
|
|
9545
|
-
const
|
|
10247
|
+
const lineage = branchLineage(scope);
|
|
10248
|
+
const params = [scope.projectId, scope.sessionId];
|
|
10249
|
+
let branchClause = "AND branch_head_id IS NULL";
|
|
10250
|
+
if (lineage.length > 0) {
|
|
10251
|
+
branchClause = "AND branch_head_id IN (" + lineage.map(() => "?").join(",") + ")";
|
|
10252
|
+
params.push(...lineage);
|
|
10253
|
+
}
|
|
10254
|
+
if (kind)
|
|
10255
|
+
params.push(kind);
|
|
10256
|
+
if (key)
|
|
10257
|
+
params.push(key);
|
|
10258
|
+
return db.query(`
|
|
9546
10259
|
SELECT * FROM context_nodes
|
|
9547
10260
|
WHERE project_id = ? AND session_id = ? AND source = 'compaction'
|
|
9548
|
-
${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
|
|
9549
|
-
`).all(
|
|
9550
|
-
return rows.filter((row) => lineage.size > 0 ? Boolean(row.branch_head_id && lineage.has(row.branch_head_id)) : row.branch_head_id == null);
|
|
10261
|
+
${branchClause} ${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
|
|
10262
|
+
`).all(...params);
|
|
9551
10263
|
}
|
|
9552
10264
|
function latestLineageFact(db, scope, kind, key) {
|
|
9553
10265
|
const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
|
|
@@ -9605,10 +10317,9 @@ function pruneProject(db, projectId) {
|
|
|
9605
10317
|
ORDER BY CASE WHEN status = 'active' THEN 1 ELSE 0 END, updated_at ASC
|
|
9606
10318
|
LIMIT ?
|
|
9607
10319
|
`).all(projectId, excess) : [];
|
|
9608
|
-
const removeFts = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9609
10320
|
const removeNode = db.query("DELETE FROM context_nodes WHERE id = ?");
|
|
9610
10321
|
for (const victim of victims) {
|
|
9611
|
-
|
|
10322
|
+
removeFtsNode(db, victim.id);
|
|
9612
10323
|
removeNode.run(victim.id);
|
|
9613
10324
|
}
|
|
9614
10325
|
const staleSessions = db.query(`
|
|
@@ -9619,6 +10330,7 @@ function pruneProject(db, projectId) {
|
|
|
9619
10330
|
for (const session of staleSessions)
|
|
9620
10331
|
removeNode.run(session.id);
|
|
9621
10332
|
}
|
|
10333
|
+
var activeCompactionIndexDatabase = null;
|
|
9622
10334
|
function indexCompactionState(projectId, state) {
|
|
9623
10335
|
const sessionId = state.scope?.sessionId;
|
|
9624
10336
|
if (!sessionId || state.scope?.projectId !== projectId)
|
|
@@ -9630,8 +10342,9 @@ function indexCompactionState(projectId, state) {
|
|
|
9630
10342
|
branchEntryIds: state.scope.branchAncestryIds
|
|
9631
10343
|
};
|
|
9632
10344
|
let db = null;
|
|
10345
|
+
const ownsDatabase = activeCompactionIndexDatabase === null;
|
|
9633
10346
|
try {
|
|
9634
|
-
db = openDatabase();
|
|
10347
|
+
db = activeCompactionIndexDatabase ?? openDatabase();
|
|
9635
10348
|
const transaction = db.transaction(() => {
|
|
9636
10349
|
const now = Date.now();
|
|
9637
10350
|
const projectNode = makeNode({ ...scope, sessionId: "*", branchHeadId: undefined }, "project", "Project", projectId, { confidence: 1 });
|
|
@@ -9709,9 +10422,11 @@ function indexCompactionState(projectId, state) {
|
|
|
9709
10422
|
warn("indexCompactionState failed", error2);
|
|
9710
10423
|
return false;
|
|
9711
10424
|
} finally {
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
10425
|
+
if (ownsDatabase) {
|
|
10426
|
+
try {
|
|
10427
|
+
db?.close();
|
|
10428
|
+
} catch {}
|
|
10429
|
+
}
|
|
9715
10430
|
}
|
|
9716
10431
|
}
|
|
9717
10432
|
var pendingCompactionIndexes = new Map;
|
|
@@ -9721,8 +10436,20 @@ function drainCompactionIndexes() {
|
|
|
9721
10436
|
compactionIndexTimer = null;
|
|
9722
10437
|
const jobs = [...pendingCompactionIndexes.values()];
|
|
9723
10438
|
pendingCompactionIndexes.clear();
|
|
9724
|
-
|
|
9725
|
-
|
|
10439
|
+
let db = null;
|
|
10440
|
+
try {
|
|
10441
|
+
db = openDatabase();
|
|
10442
|
+
activeCompactionIndexDatabase = db;
|
|
10443
|
+
for (const job of jobs)
|
|
10444
|
+
indexCompactionState(job.projectId, job.state);
|
|
10445
|
+
} catch (error2) {
|
|
10446
|
+
warn("context graph index drain failed", error2);
|
|
10447
|
+
} finally {
|
|
10448
|
+
activeCompactionIndexDatabase = null;
|
|
10449
|
+
try {
|
|
10450
|
+
db?.close();
|
|
10451
|
+
} catch {}
|
|
10452
|
+
}
|
|
9726
10453
|
if (pendingCompactionIndexes.size)
|
|
9727
10454
|
armCompactionIndexDrain();
|
|
9728
10455
|
}
|
|
@@ -9737,8 +10464,10 @@ function scheduleCompactionStateIndex(projectId, state) {
|
|
|
9737
10464
|
if (!sessionId || !branchHeadId || state.scope?.projectId !== projectId)
|
|
9738
10465
|
return false;
|
|
9739
10466
|
const key = projectId + "\x00" + sessionId + "\x00" + branchHeadId;
|
|
9740
|
-
if (
|
|
9741
|
-
|
|
10467
|
+
if (pendingCompactionIndexes.has(key))
|
|
10468
|
+
pendingCompactionIndexes.delete(key);
|
|
10469
|
+
if (pendingCompactionIndexes.size >= MAX_PENDING_COMPACTION_INDEXES) {
|
|
10470
|
+
warn("context graph index queue full; new derived update was rejected");
|
|
9742
10471
|
return false;
|
|
9743
10472
|
}
|
|
9744
10473
|
pendingCompactionIndexes.set(key, { projectId, state });
|
|
@@ -9760,9 +10489,8 @@ function closeContextMemory(projectId, kind, content, status) {
|
|
|
9760
10489
|
UPDATE context_nodes SET status = ?, updated_at = ?
|
|
9761
10490
|
WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND status = 'active'
|
|
9762
10491
|
`).run(status, Date.now(), projectId, kind, factKey(content));
|
|
9763
|
-
const remove = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9764
10492
|
for (const row of rows)
|
|
9765
|
-
|
|
10493
|
+
removeFtsNode(db, row.id);
|
|
9766
10494
|
});
|
|
9767
10495
|
transaction();
|
|
9768
10496
|
return rows.length;
|
|
@@ -9808,10 +10536,9 @@ function saveContextMemory(scope, memory) {
|
|
|
9808
10536
|
...duplicates.flatMap((item) => parsePaths(item.related_paths))
|
|
9809
10537
|
])).slice(0, 20);
|
|
9810
10538
|
upsertNode(db, node, true);
|
|
9811
|
-
const removeFts = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9812
10539
|
const removeNode = db.query("DELETE FROM context_nodes WHERE id = ?");
|
|
9813
10540
|
for (const duplicate of duplicates) {
|
|
9814
|
-
|
|
10541
|
+
removeFtsNode(db, duplicate.id);
|
|
9815
10542
|
removeNode.run(duplicate.id);
|
|
9816
10543
|
}
|
|
9817
10544
|
for (const file of relatedPaths) {
|
|
@@ -9838,7 +10565,7 @@ function searchRows(db, projectId, terms) {
|
|
|
9838
10565
|
try {
|
|
9839
10566
|
return db.query(`
|
|
9840
10567
|
SELECT n.* FROM context_nodes_fts f
|
|
9841
|
-
JOIN context_nodes n ON n.
|
|
10568
|
+
JOIN context_nodes n ON n.rowid = f.rowid
|
|
9842
10569
|
WHERE context_nodes_fts MATCH ? AND n.project_id = ? AND n.status = 'active'
|
|
9843
10570
|
AND n.kind NOT IN ('project', 'session')
|
|
9844
10571
|
ORDER BY bm25(context_nodes_fts, 0.0, 3.0, 1.0, 0.5)
|
|
@@ -10057,6 +10784,20 @@ function aggregateProviderRoutes(metrics) {
|
|
|
10057
10784
|
}
|
|
10058
10785
|
|
|
10059
10786
|
// src/app/steps/metrics.ts
|
|
10787
|
+
var KNOWN_VERIFICATION_GAP_KINDS = {
|
|
10788
|
+
"missing-section": true,
|
|
10789
|
+
"missing-file": true,
|
|
10790
|
+
"missing-read-file": true,
|
|
10791
|
+
"missing-deleted-file": true,
|
|
10792
|
+
"missing-error": true,
|
|
10793
|
+
"missing-constraint": true,
|
|
10794
|
+
"missing-decision": true,
|
|
10795
|
+
"missing-goal": true,
|
|
10796
|
+
"fabricated-file": true,
|
|
10797
|
+
inconsistency: true,
|
|
10798
|
+
"missing-open-loops": true,
|
|
10799
|
+
"unsupported-claim": true
|
|
10800
|
+
};
|
|
10060
10801
|
function runType(rc) {
|
|
10061
10802
|
return rc.flags.skipCompact ? "tool" : rc.flags.autoTriggered ? "auto" : "manual";
|
|
10062
10803
|
}
|
|
@@ -10130,8 +10871,8 @@ function buildSuccessMetrics(rc, status) {
|
|
|
10130
10871
|
adapted: rc.adapted
|
|
10131
10872
|
};
|
|
10132
10873
|
}
|
|
10133
|
-
function recordSuccessMetrics(rc, status) {
|
|
10134
|
-
appendMetricsSnapshot(rc.sessionId, buildSuccessMetrics(rc, status));
|
|
10874
|
+
async function recordSuccessMetrics(rc, status) {
|
|
10875
|
+
await appendMetricsSnapshot(rc.sessionId, buildSuccessMetrics(rc, status));
|
|
10135
10876
|
const ecs = getExtractionCacheStats(rc.services);
|
|
10136
10877
|
const ms = getMetricsSummary(rc.services);
|
|
10137
10878
|
if (status === "success" && ms.totalCalls > 0) {
|
|
@@ -10142,30 +10883,19 @@ function recordSuccessMetrics(rc, status) {
|
|
|
10142
10883
|
rc.notify("Metrics: " + ms.totalCalls + " calls, " + inputLabel + ", " + ms.totalOutput + "t out, provider-cache " + providerCacheRate + "% (internal phases disabled), extraction-cache " + extractionCacheRate + "%, " + ms.avgLatency + "ms avg", "info");
|
|
10143
10884
|
}
|
|
10144
10885
|
}
|
|
10145
|
-
function recordFailureMetrics(rc, err, fields) {
|
|
10886
|
+
async function recordFailureMetrics(rc, err, fields) {
|
|
10146
10887
|
const releaseChannel = rc.config?.telemetryChannel ?? loadConfig().telemetryChannel;
|
|
10147
10888
|
const failureKind = classifyTelemetryFailure(err, rc.cancellation.timedOut);
|
|
10148
10889
|
const gate = err && typeof err === "object" ? err : null;
|
|
10149
10890
|
const finite2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10150
10891
|
const softKinds = new Set(["recent-user-turn", "anchor", "topical"]);
|
|
10151
10892
|
const relaxedSoftBoundaries = Array.isArray(gate?.relaxedSoftBoundaries) ? gate.relaxedSoftBoundaries.filter((kind) => typeof kind === "string" && softKinds.has(kind)) : undefined;
|
|
10152
|
-
const
|
|
10153
|
-
|
|
10154
|
-
"missing-file",
|
|
10155
|
-
"missing-error",
|
|
10156
|
-
"missing-constraint",
|
|
10157
|
-
"missing-decision",
|
|
10158
|
-
"missing-goal",
|
|
10159
|
-
"fabricated-file",
|
|
10160
|
-
"inconsistency",
|
|
10161
|
-
"missing-open-loops",
|
|
10162
|
-
"unsupported-claim"
|
|
10163
|
-
]);
|
|
10164
|
-
const gapKinds = Array.isArray(gate?.gapKinds) ? gate.gapKinds.filter((kind) => typeof kind === "string" && knownGapKinds.has(kind)) : undefined;
|
|
10893
|
+
const gapKinds = Array.isArray(gate?.gapKinds) ? gate.gapKinds.filter((kind) => typeof kind === "string" && Object.hasOwn(KNOWN_VERIFICATION_GAP_KINDS, kind)) : undefined;
|
|
10894
|
+
const verificationStage = gate?.stage === "post-synthesis" || gate?.stage === "post-state" ? gate.stage : undefined;
|
|
10165
10895
|
const verificationScore = typeof gate?.score === "number" && Number.isFinite(gate.score) ? gate.score : undefined;
|
|
10166
10896
|
const initialVerificationScore = typeof gate?.initialScore === "number" && Number.isFinite(gate.initialScore) ? gate.initialScore : undefined;
|
|
10167
10897
|
const verificationGaps = typeof gate?.gapCount === "number" && Number.isInteger(gate.gapCount) && gate.gapCount >= 0 ? gate.gapCount : undefined;
|
|
10168
|
-
appendMetricsLog(fields.sessionId ?? "unknown", {
|
|
10898
|
+
await appendMetricsLog(fields.sessionId ?? "unknown", {
|
|
10169
10899
|
runId: rc.runId,
|
|
10170
10900
|
metricsSchemaVersion: 2,
|
|
10171
10901
|
version: VERSION,
|
|
@@ -10201,6 +10931,7 @@ function recordFailureMetrics(rc, err, fields) {
|
|
|
10201
10931
|
verificationGaps,
|
|
10202
10932
|
remainingVerificationGaps: verificationGaps,
|
|
10203
10933
|
verificationGapKinds: gapKinds,
|
|
10934
|
+
verificationStage,
|
|
10204
10935
|
phaseTimings: rc.phaseTimings,
|
|
10205
10936
|
durationMs: Date.now() - rc.pipelineStart
|
|
10206
10937
|
}, rc.services);
|
|
@@ -10219,7 +10950,7 @@ function compactText(error2) {
|
|
|
10219
10950
|
function formatCompactErrorForUi(error2) {
|
|
10220
10951
|
if (error2 instanceof VerificationGateError) {
|
|
10221
10952
|
const kinds = error2.gapKinds.slice(0, 4).join(", ") || "unknown";
|
|
10222
|
-
return "Verification stopped apply: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. " + DEBUG_HINT;
|
|
10953
|
+
return "Verification stopped apply at the " + error2.stage + " gate: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. " + DEBUG_HINT;
|
|
10223
10954
|
}
|
|
10224
10955
|
if (error2 instanceof YieldGateError) {
|
|
10225
10956
|
const reason = error2.reason === "target-miss" ? "target missed" : "saving below 10%";
|
|
@@ -10252,7 +10983,7 @@ async function commitAppliedCompaction(pending) {
|
|
|
10252
10983
|
failures.push("conversation backup");
|
|
10253
10984
|
if (!pending.metricsSnapshot)
|
|
10254
10985
|
return failures;
|
|
10255
|
-
appendMetricsSnapshot(pending.sessionId, {
|
|
10986
|
+
await appendMetricsSnapshot(pending.sessionId, {
|
|
10256
10987
|
...pending.metricsSnapshot,
|
|
10257
10988
|
persistenceStatus: failures.length ? "partial" : "complete",
|
|
10258
10989
|
persistenceFailures: failures.length ? failures : undefined,
|
|
@@ -10289,10 +11020,14 @@ function runDamageDetection(rc) {
|
|
|
10289
11020
|
}
|
|
10290
11021
|
}
|
|
10291
11022
|
function stagePendingCompaction(rc, metricsSnapshot) {
|
|
11023
|
+
const originBranchHeadId = branchEntryIds(rc.branch).at(-1);
|
|
11024
|
+
if (!originBranchHeadId)
|
|
11025
|
+
throw new Error("Pending compaction requires an identifiable branch head");
|
|
10292
11026
|
const pending = {
|
|
10293
11027
|
runId: rc.runId,
|
|
10294
11028
|
summary: rc.finalSummary,
|
|
10295
11029
|
firstKeptEntryId: rc.firstKeptId,
|
|
11030
|
+
originBranchHeadId,
|
|
10296
11031
|
tokensBefore: rc.totalTokens,
|
|
10297
11032
|
details: rc.details,
|
|
10298
11033
|
metricsSnapshot,
|
|
@@ -10349,7 +11084,9 @@ function makeBase(opts) {
|
|
|
10349
11084
|
};
|
|
10350
11085
|
const pipelineStart = Date.now();
|
|
10351
11086
|
const requestedMode = opts.mode ?? modeFromLegacyProfile(opts.profile ?? "balanced");
|
|
10352
|
-
const
|
|
11087
|
+
const usage = opts.ctx.getContextUsage();
|
|
11088
|
+
const reportedPercent = usage?.percent;
|
|
11089
|
+
const contextPercent = Number.isFinite(reportedPercent) && (reportedPercent ?? 0) >= 0 ? reportedPercent : safeContextPercent(usage?.tokens, opts.ctx.model?.contextWindow);
|
|
10353
11090
|
const mode = resolveMode(requestedMode, contextPercent);
|
|
10354
11091
|
const profile = opts.mode ? MODE_POLICIES[mode].profile : opts.profile ?? MODE_POLICIES[mode].profile;
|
|
10355
11092
|
return {
|
|
@@ -10483,7 +11220,7 @@ async function runSmartCompact(opts) {
|
|
|
10483
11220
|
stated.vlog("Pipeline complete \u2014 method=" + stated.method + " calls=" + stated.llmCalls + " chunks=" + stated.chunkCount + " tokensSaved=" + stated.tokensSaved);
|
|
10484
11221
|
markPhase(stated, "state");
|
|
10485
11222
|
if (stated.flags.dryRun) {
|
|
10486
|
-
recordSuccessMetrics(stated, "dry-run");
|
|
11223
|
+
await recordSuccessMetrics(stated, "dry-run");
|
|
10487
11224
|
stated.ctx.ui.notify("DRY RUN (" + stated.method + ", " + stated.mode + ") \u2014 " + stated.toCompact.length + " msgs, " + stated.llmCalls + " calls", "info");
|
|
10488
11225
|
return { kind: "dry-run", details: stated.details };
|
|
10489
11226
|
}
|
|
@@ -10506,7 +11243,7 @@ async function runSmartCompact(opts) {
|
|
|
10506
11243
|
}
|
|
10507
11244
|
if (decision !== "apply") {
|
|
10508
11245
|
stated.pendingRef.clear(stated.sessionId);
|
|
10509
|
-
recordSuccessMetrics(stated, "cancelled");
|
|
11246
|
+
await recordSuccessMetrics(stated, "cancelled");
|
|
10510
11247
|
stated.ctx.ui.notify("Compaction cancelled \u2014 current conversation unchanged", "info");
|
|
10511
11248
|
return { kind: "cancelled", source: "user" };
|
|
10512
11249
|
}
|
|
@@ -10532,7 +11269,7 @@ async function runSmartCompact(opts) {
|
|
|
10532
11269
|
return willApply ? { kind: "apply-requested", pending } : { kind: "staged", pending };
|
|
10533
11270
|
} catch (err) {
|
|
10534
11271
|
runFailed = true;
|
|
10535
|
-
recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
11272
|
+
await recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
10536
11273
|
throw err;
|
|
10537
11274
|
} finally {
|
|
10538
11275
|
opts.abortSignal?.removeEventListener("abort", abortFromHost);
|
|
@@ -10725,55 +11462,70 @@ function parseSmartCompactTool(params) {
|
|
|
10725
11462
|
function createPendingSlot(opts) {
|
|
10726
11463
|
const ttlMs = opts.ttlMs;
|
|
10727
11464
|
const now = opts.now ?? Date.now;
|
|
10728
|
-
const maxEntries = Math.max(1, opts.maxEntries ??
|
|
11465
|
+
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
10729
11466
|
const entries = new Map;
|
|
11467
|
+
let newestSessionId = null;
|
|
11468
|
+
const refreshNewest = () => {
|
|
11469
|
+
newestSessionId = null;
|
|
11470
|
+
for (const sessionId of entries.keys())
|
|
11471
|
+
newestSessionId = sessionId;
|
|
11472
|
+
};
|
|
11473
|
+
const deleteEntry = (sessionId) => {
|
|
11474
|
+
if (!entries.delete(sessionId))
|
|
11475
|
+
return;
|
|
11476
|
+
if (newestSessionId === sessionId)
|
|
11477
|
+
refreshNewest();
|
|
11478
|
+
};
|
|
10730
11479
|
const prune = () => {
|
|
10731
|
-
const
|
|
11480
|
+
const current = now();
|
|
11481
|
+
let removedNewest = false;
|
|
10732
11482
|
for (const [sessionId, entry] of entries) {
|
|
10733
|
-
if (
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
let result;
|
|
10739
|
-
for (const entry of entries.values()) {
|
|
10740
|
-
if (!result || entry.createdAt >= result.createdAt)
|
|
10741
|
-
result = entry;
|
|
11483
|
+
if (current - entry.createdAt <= ttlMs)
|
|
11484
|
+
continue;
|
|
11485
|
+
entries.delete(sessionId);
|
|
11486
|
+
if (newestSessionId === sessionId)
|
|
11487
|
+
removedNewest = true;
|
|
10742
11488
|
}
|
|
10743
|
-
|
|
11489
|
+
if (removedNewest)
|
|
11490
|
+
refreshNewest();
|
|
10744
11491
|
};
|
|
10745
11492
|
return {
|
|
10746
11493
|
set(pending) {
|
|
10747
11494
|
prune();
|
|
10748
11495
|
entries.delete(pending.sessionId);
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
11496
|
+
entries.set(pending.sessionId, { value: pending, createdAt: now() });
|
|
11497
|
+
newestSessionId = pending.sessionId;
|
|
11498
|
+
while (entries.size > maxEntries) {
|
|
11499
|
+
const oldest = entries.keys().next().value;
|
|
11500
|
+
if (oldest === undefined)
|
|
10752
11501
|
break;
|
|
10753
|
-
|
|
11502
|
+
deleteEntry(oldest);
|
|
10754
11503
|
}
|
|
10755
|
-
entries.set(pending.sessionId, { value: pending, createdAt: now() });
|
|
10756
11504
|
},
|
|
10757
11505
|
consume(ctx) {
|
|
10758
11506
|
const currentSessionId = resolveSessionId(ctx);
|
|
10759
11507
|
const entry = entries.get(currentSessionId);
|
|
10760
|
-
if (
|
|
10761
|
-
const
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
11508
|
+
if (entry) {
|
|
11509
|
+
const ageMs = now() - entry.createdAt;
|
|
11510
|
+
if (ageMs > ttlMs) {
|
|
11511
|
+
deleteEntry(currentSessionId);
|
|
11512
|
+
prune();
|
|
11513
|
+
return { kind: "expired", ageMs };
|
|
11514
|
+
}
|
|
11515
|
+
deleteEntry(currentSessionId);
|
|
11516
|
+
return { kind: "ok", pending: entry.value };
|
|
10768
11517
|
}
|
|
10769
|
-
|
|
10770
|
-
|
|
11518
|
+
prune();
|
|
11519
|
+
const other = newestSessionId == null ? undefined : entries.get(newestSessionId);
|
|
11520
|
+
return other ? { kind: "mismatch", expected: other.value.sessionId, actual: currentSessionId } : { kind: "empty" };
|
|
10771
11521
|
},
|
|
10772
11522
|
clear(sessionId) {
|
|
10773
11523
|
if (sessionId)
|
|
10774
|
-
|
|
10775
|
-
else
|
|
11524
|
+
deleteEntry(sessionId);
|
|
11525
|
+
else {
|
|
10776
11526
|
entries.clear();
|
|
11527
|
+
newestSessionId = null;
|
|
11528
|
+
}
|
|
10777
11529
|
},
|
|
10778
11530
|
isPresent(sessionId) {
|
|
10779
11531
|
prune();
|
|
@@ -10781,7 +11533,8 @@ function createPendingSlot(opts) {
|
|
|
10781
11533
|
},
|
|
10782
11534
|
peek(sessionId) {
|
|
10783
11535
|
prune();
|
|
10784
|
-
|
|
11536
|
+
const entry = sessionId ? entries.get(sessionId) : newestSessionId == null ? undefined : entries.get(newestSessionId);
|
|
11537
|
+
return entry?.value ?? null;
|
|
10785
11538
|
},
|
|
10786
11539
|
size() {
|
|
10787
11540
|
prune();
|
|
@@ -10861,18 +11614,30 @@ function createCompactionCommitStore(options = {}) {
|
|
|
10861
11614
|
// src/app/native-continuity-bridge.ts
|
|
10862
11615
|
import crypto6 from "crypto";
|
|
10863
11616
|
import fs10 from "fs";
|
|
10864
|
-
import
|
|
11617
|
+
import path14 from "path";
|
|
10865
11618
|
var MAX_TEXT_BYTES = 256 * 1024;
|
|
10866
11619
|
function sameScope(a, b) {
|
|
10867
11620
|
return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
|
|
10868
11621
|
}
|
|
11622
|
+
function boundedContinuityText(text) {
|
|
11623
|
+
const bytes = Buffer.from(text);
|
|
11624
|
+
if (bytes.length <= MAX_TEXT_BYTES)
|
|
11625
|
+
return text;
|
|
11626
|
+
const marker = Buffer.from(`
|
|
11627
|
+
\u2026 [continuity truncated from ` + bytes.length + ` bytes]
|
|
11628
|
+
`);
|
|
11629
|
+
let end = Math.max(0, MAX_TEXT_BYTES - marker.length);
|
|
11630
|
+
while (end > 0 && (bytes[end] & 192) === 128)
|
|
11631
|
+
end--;
|
|
11632
|
+
return Buffer.concat([bytes.subarray(0, end), marker]).toString("utf8");
|
|
11633
|
+
}
|
|
10869
11634
|
function createNativeContinuityBridge(opts = {}) {
|
|
10870
11635
|
const ttlMs = Math.max(1, opts.ttlMs ?? SEVEN_DAYS_MS);
|
|
10871
11636
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
10872
11637
|
const now = opts.now ?? Date.now;
|
|
10873
11638
|
const dir = opts.dir ?? nativeContinuityDir();
|
|
10874
|
-
const lockTarget =
|
|
10875
|
-
const fileFor = (scope) =>
|
|
11639
|
+
const lockTarget = path14.join(dir, "bridge");
|
|
11640
|
+
const fileFor = (scope) => path14.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
|
|
10876
11641
|
const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
|
|
10877
11642
|
const readEntry = (file) => {
|
|
10878
11643
|
try {
|
|
@@ -10897,7 +11662,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10897
11662
|
for (const name of names) {
|
|
10898
11663
|
if (!/\.tmp\.\d+\.[0-9a-f]+$/i.test(name))
|
|
10899
11664
|
continue;
|
|
10900
|
-
const file =
|
|
11665
|
+
const file = path14.join(dir, name);
|
|
10901
11666
|
try {
|
|
10902
11667
|
if (now() - fs10.statSync(file).mtimeMs > ONE_HOUR_MS)
|
|
10903
11668
|
fs10.unlinkSync(file);
|
|
@@ -10905,7 +11670,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10905
11670
|
}
|
|
10906
11671
|
const files = names.filter((file) => file.endsWith(".json"));
|
|
10907
11672
|
for (const name of files) {
|
|
10908
|
-
const file =
|
|
11673
|
+
const file = path14.join(dir, name);
|
|
10909
11674
|
const entry = readEntry(file);
|
|
10910
11675
|
if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
|
|
10911
11676
|
try {
|
|
@@ -10939,8 +11704,9 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10939
11704
|
};
|
|
10940
11705
|
return {
|
|
10941
11706
|
stage(scope, text) {
|
|
10942
|
-
if (!validScope(scope) || !text.trim()
|
|
11707
|
+
if (!validScope(scope) || !text.trim())
|
|
10943
11708
|
return;
|
|
11709
|
+
const boundedText = boundedContinuityText(text);
|
|
10944
11710
|
try {
|
|
10945
11711
|
locked(() => {
|
|
10946
11712
|
const target = fileFor(scope);
|
|
@@ -10948,7 +11714,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10948
11714
|
fs10.unlinkSync(target);
|
|
10949
11715
|
} catch {}
|
|
10950
11716
|
prune(1);
|
|
10951
|
-
const entry = { schemaVersion: 1, scope, text, createdAt: now() };
|
|
11717
|
+
const entry = { schemaVersion: 1, scope, text: boundedText, createdAt: now() };
|
|
10952
11718
|
atomicWriteFileSync(target, JSON.stringify(entry));
|
|
10953
11719
|
try {
|
|
10954
11720
|
fs10.chmodSync(target, 384);
|
|
@@ -11010,11 +11776,83 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
11010
11776
|
};
|
|
11011
11777
|
}
|
|
11012
11778
|
|
|
11779
|
+
// src/app/settled-auto-trigger.ts
|
|
11780
|
+
function createSettledAutoTrigger(options = {}) {
|
|
11781
|
+
const now = options.now ?? Date.now;
|
|
11782
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
11783
|
+
const active = new Map;
|
|
11784
|
+
const lastCompactionAt = new Map;
|
|
11785
|
+
const noteCompaction = (sessionId) => {
|
|
11786
|
+
if (!isUnresolvedSessionId(sessionId))
|
|
11787
|
+
lastCompactionAt.set(sessionId, now());
|
|
11788
|
+
};
|
|
11789
|
+
const clear = (sessionId) => {
|
|
11790
|
+
active.delete(sessionId);
|
|
11791
|
+
lastCompactionAt.delete(sessionId);
|
|
11792
|
+
};
|
|
11793
|
+
const request = async (ctx, config) => {
|
|
11794
|
+
if (!config.autoTrigger || config.autoTriggerStrategy !== "settled")
|
|
11795
|
+
return;
|
|
11796
|
+
const sessionId = resolveSessionId(ctx);
|
|
11797
|
+
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
11798
|
+
return;
|
|
11799
|
+
const usage = ctx.getContextUsage();
|
|
11800
|
+
const totalTokens = usage?.tokens;
|
|
11801
|
+
if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens < MIN_TOKEN_THRESHOLD || !ctx.model)
|
|
11802
|
+
return;
|
|
11803
|
+
const contextPercent = safeContextPercent(totalTokens, ctx.model.contextWindow);
|
|
11804
|
+
if (contextPercent < config.minContextPercent)
|
|
11805
|
+
return;
|
|
11806
|
+
const lastCompaction = lastCompactionAt.get(sessionId);
|
|
11807
|
+
if (lastCompaction !== undefined && now() - lastCompaction < cooldownMs)
|
|
11808
|
+
return;
|
|
11809
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
11810
|
+
return;
|
|
11811
|
+
const requestToken = Symbol(sessionId);
|
|
11812
|
+
active.set(sessionId, requestToken);
|
|
11813
|
+
await new Promise((resolve2) => {
|
|
11814
|
+
let finished = false;
|
|
11815
|
+
const finish = () => {
|
|
11816
|
+
if (finished)
|
|
11817
|
+
return;
|
|
11818
|
+
finished = true;
|
|
11819
|
+
if (active.get(sessionId) === requestToken)
|
|
11820
|
+
active.delete(sessionId);
|
|
11821
|
+
resolve2();
|
|
11822
|
+
};
|
|
11823
|
+
try {
|
|
11824
|
+
ctx.compact({
|
|
11825
|
+
onComplete: () => {
|
|
11826
|
+
if (active.get(sessionId) === requestToken)
|
|
11827
|
+
noteCompaction(sessionId);
|
|
11828
|
+
finish();
|
|
11829
|
+
},
|
|
11830
|
+
onError: (error2) => {
|
|
11831
|
+
debugError("Settled smart compact request failed", error2);
|
|
11832
|
+
finish();
|
|
11833
|
+
}
|
|
11834
|
+
});
|
|
11835
|
+
} catch (error2) {
|
|
11836
|
+
debugError("Settled smart compact request failed", error2);
|
|
11837
|
+
finish();
|
|
11838
|
+
}
|
|
11839
|
+
});
|
|
11840
|
+
};
|
|
11841
|
+
return { request, noteCompaction, clear };
|
|
11842
|
+
}
|
|
11843
|
+
|
|
11013
11844
|
// src/index.ts
|
|
11014
11845
|
function unwrapConsumed(result, ctx) {
|
|
11015
11846
|
switch (result.kind) {
|
|
11016
|
-
case "ok":
|
|
11847
|
+
case "ok": {
|
|
11848
|
+
const activeEntryIds = new Set(branchEntryIds(ctx.sessionManager.getBranch()));
|
|
11849
|
+
if (!activeEntryIds.has(result.pending.originBranchHeadId) || !activeEntryIds.has(result.pending.firstKeptEntryId)) {
|
|
11850
|
+
warn("Discarding pending smart compaction prepared for a divergent branch");
|
|
11851
|
+
ctx.ui.notify("Divergent-branch pending smart compaction discarded", "warning");
|
|
11852
|
+
return null;
|
|
11853
|
+
}
|
|
11017
11854
|
return result.pending;
|
|
11855
|
+
}
|
|
11018
11856
|
case "empty":
|
|
11019
11857
|
return null;
|
|
11020
11858
|
case "expired":
|
|
@@ -11069,6 +11907,7 @@ function smartCompactExtension(pi) {
|
|
|
11069
11907
|
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
11070
11908
|
const isRunning = createSessionRunLock();
|
|
11071
11909
|
const damageMonitor = new OnlineDamageMonitor;
|
|
11910
|
+
const settledAutoTrigger = createSettledAutoTrigger();
|
|
11072
11911
|
const nativeContinuity = createNativeContinuityBridge();
|
|
11073
11912
|
const recordApplyFailure = (pending, reason) => {
|
|
11074
11913
|
if (!pending.metricsSnapshot)
|
|
@@ -11181,7 +12020,7 @@ function smartCompactExtension(pi) {
|
|
|
11181
12020
|
const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
|
|
11182
12021
|
const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
|
|
11183
12022
|
const content = scrubber.scrubText(params.content).value;
|
|
11184
|
-
const relatedPaths = (params.related_paths ?? []).map((
|
|
12023
|
+
const relatedPaths = (params.related_paths ?? []).map((path15) => scrubber.scrubText(path15).value);
|
|
11185
12024
|
const status = params.status ?? "active";
|
|
11186
12025
|
if (!ctx.hasUI) {
|
|
11187
12026
|
return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
|
|
@@ -11219,7 +12058,10 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11219
12058
|
await ctx.waitForIdle();
|
|
11220
12059
|
try {
|
|
11221
12060
|
const knownProviders = new Set(ctx.modelRegistry.getAvailable().map((model) => model.provider));
|
|
11222
|
-
const parsedInput = parseSmartCompactCommand(args, (token) =>
|
|
12061
|
+
const parsedInput = parseSmartCompactCommand(args, (token) => {
|
|
12062
|
+
const [provider, ...modelPath] = token.split("/");
|
|
12063
|
+
return /^[a-z0-9_.-]+$/i.test(provider) && modelPath.length > 0 && modelPath.every((segment) => /^[a-z0-9_.:-]+$/i.test(segment)) && Boolean(findModelById(ctx, token) || knownProviders.has(provider));
|
|
12064
|
+
});
|
|
11223
12065
|
if (!parsedInput.ok) {
|
|
11224
12066
|
ctx.ui.notify(parsedInput.error, "error");
|
|
11225
12067
|
return;
|
|
@@ -11366,7 +12208,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11366
12208
|
if (!args.trim()) {
|
|
11367
12209
|
const usage = ctx.getContextUsage();
|
|
11368
12210
|
const totalTokens = usage?.tokens ?? 0;
|
|
11369
|
-
const pct =
|
|
12211
|
+
const pct = Math.round(safeContextPercent(totalTokens, ctx.model?.contextWindow));
|
|
11370
12212
|
const cur = ctx.model;
|
|
11371
12213
|
const initialRoutes = resolveModels(ctx, cur, config);
|
|
11372
12214
|
if (!initialRoutes.sumModel) {
|
|
@@ -11429,6 +12271,13 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11429
12271
|
}
|
|
11430
12272
|
}
|
|
11431
12273
|
});
|
|
12274
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
12275
|
+
try {
|
|
12276
|
+
await settledAutoTrigger.request(ctx, loadConfig());
|
|
12277
|
+
} catch (error2) {
|
|
12278
|
+
debugError("Settled smart compact trigger stopped", error2);
|
|
12279
|
+
}
|
|
12280
|
+
});
|
|
11432
12281
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
11433
12282
|
const consumed = unwrapConsumed(pendingRef.consume(ctx), ctx);
|
|
11434
12283
|
if (consumed && stageForNativeApply(consumed, event.signal)) {
|
|
@@ -11442,7 +12291,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11442
12291
|
const totalTokens = usage?.tokens ?? 0;
|
|
11443
12292
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
|
|
11444
12293
|
return;
|
|
11445
|
-
const pct =
|
|
12294
|
+
const pct = safeContextPercent(totalTokens, ctx.model?.contextWindow);
|
|
11446
12295
|
if (event.reason !== "overflow" && pct < config.minContextPercent)
|
|
11447
12296
|
return;
|
|
11448
12297
|
const cur = ctx.model;
|
|
@@ -11475,6 +12324,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11475
12324
|
overflowRecovery: event.reason === "overflow",
|
|
11476
12325
|
maxLlmCalls: Math.min(config.maxLlmCalls, AUTO_TRIGGER_MAX_LLM_CALLS),
|
|
11477
12326
|
timeoutMs: effectiveTimeoutMs,
|
|
12327
|
+
abortSignal: event.signal,
|
|
11478
12328
|
cancellationOut
|
|
11479
12329
|
});
|
|
11480
12330
|
} catch (err) {
|
|
@@ -11493,6 +12343,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11493
12343
|
});
|
|
11494
12344
|
pi.on("session_compact", async (event, ctx) => {
|
|
11495
12345
|
const sessionId = resolveSessionId(ctx);
|
|
12346
|
+
settledAutoTrigger.noteCompaction(sessionId);
|
|
11496
12347
|
if (event.fromExtension) {
|
|
11497
12348
|
const details = event.compactionEntry.details;
|
|
11498
12349
|
const runId = typeof details?.runId === "string" ? details.runId : null;
|
|
@@ -11578,6 +12429,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11578
12429
|
damageMonitor.clear(sessionId);
|
|
11579
12430
|
pendingRef.clear(sessionId);
|
|
11580
12431
|
commitCandidates.clearSession(sessionId, "shutdown");
|
|
12432
|
+
settledAutoTrigger.clear(sessionId);
|
|
11581
12433
|
});
|
|
11582
12434
|
pi.registerTool({
|
|
11583
12435
|
name: "smart_compact",
|
|
@@ -11635,12 +12487,12 @@ Dashboard: ` + fp : "") }], details: undefined };
|
|
|
11635
12487
|
}
|
|
11636
12488
|
const usage = ctx.getContextUsage?.();
|
|
11637
12489
|
const totalTokens = usage?.tokens ?? 0;
|
|
11638
|
-
const
|
|
11639
|
-
const pct = Math.round(
|
|
12490
|
+
const contextPercent = safeContextPercent(totalTokens, ctx.model?.contextWindow);
|
|
12491
|
+
const pct = Math.round(contextPercent);
|
|
11640
12492
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
11641
12493
|
return { content: [{ type: "text", text: "Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + pct + "%). No action needed." }], details: undefined };
|
|
11642
12494
|
}
|
|
11643
|
-
if (
|
|
12495
|
+
if (contextPercent < config.minContextPercent) {
|
|
11644
12496
|
return { content: [{ type: "text", text: "Context is only " + pct + "% full (" + totalTokens.toLocaleString() + " tokens). Compaction is not needed yet. The tool=97% in status means tool output ratio, NOT context usage." }], details: undefined };
|
|
11645
12497
|
}
|
|
11646
12498
|
const cur = ctx.model;
|