pi-smart-compact 9.0.0 → 9.1.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 +45 -35
- package/CHANGELOG.md +17 -0
- 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/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 +5 -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 +1109 -347
- 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.map +1 -1
- package/dist/provider-eval.js +214 -33
- package/dist/provider-scenario-eval.js +277 -57
- package/dist/telemetry-report.js +214 -33
- package/dist/types.d.ts +6 -0
- package/dist/types.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.1.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;
|
|
@@ -225,6 +225,7 @@ var BACKUP_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
|
225
225
|
var FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
226
226
|
var ONE_HOUR_MS = 60 * 60 * 1000;
|
|
227
227
|
var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
228
|
+
var STATE_SNAPSHOT_MAX_FILES = 64;
|
|
228
229
|
var EXTRACTION_LIMITS = {
|
|
229
230
|
MODIFIED_FILES: 120,
|
|
230
231
|
READ_FILES: 160,
|
|
@@ -232,8 +233,10 @@ var EXTRACTION_LIMITS = {
|
|
|
232
233
|
ERRORS: 80,
|
|
233
234
|
DECISIONS: 80,
|
|
234
235
|
CONSTRAINTS: 80,
|
|
236
|
+
TOPICS: 80,
|
|
235
237
|
TIMELINE: 120,
|
|
236
|
-
MEDIA_ATTACHMENTS: 40
|
|
238
|
+
MEDIA_ATTACHMENTS: 40,
|
|
239
|
+
REFERENCED_FILES: 200
|
|
237
240
|
};
|
|
238
241
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
239
242
|
var EXTRACTION_CACHE_PREFIX = "compact-extraction-";
|
|
@@ -285,6 +288,7 @@ var TRUNC = {
|
|
|
285
288
|
FINGERPRINT_SEG: 2
|
|
286
289
|
};
|
|
287
290
|
var MAX_TOOL_OUTPUT_CHARS = 800;
|
|
291
|
+
var MAX_EXPLORER_OUTPUT_CHARS = 12000;
|
|
288
292
|
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
293
|
var ERROR_RETRY_WINDOW = 6;
|
|
290
294
|
var ERROR_RESOLVE_WINDOW = 10;
|
|
@@ -485,6 +489,51 @@ function appendLineLocked(target, line, maxBytes) {
|
|
|
485
489
|
release();
|
|
486
490
|
}
|
|
487
491
|
}
|
|
492
|
+
async function appendLineLockedAsync(target, line, maxBytes) {
|
|
493
|
+
await ensureDirAsync(path.dirname(target));
|
|
494
|
+
const payload = Buffer.from(line.endsWith(`
|
|
495
|
+
`) ? line : line + `
|
|
496
|
+
`);
|
|
497
|
+
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
|
|
498
|
+
throw new Error("maxBytes must be a positive safe integer");
|
|
499
|
+
}
|
|
500
|
+
if (maxBytes !== undefined && payload.length > maxBytes) {
|
|
501
|
+
throw new Error("Log entry exceeds retention cap for " + target);
|
|
502
|
+
}
|
|
503
|
+
const release = await acquireLock(target);
|
|
504
|
+
try {
|
|
505
|
+
let stat = null;
|
|
506
|
+
try {
|
|
507
|
+
stat = await fsp.stat(target);
|
|
508
|
+
} catch (error2) {
|
|
509
|
+
if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
|
|
510
|
+
throw error2;
|
|
511
|
+
}
|
|
512
|
+
if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
|
|
513
|
+
const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
|
|
514
|
+
const buffer = Buffer.allocUnsafe(retainedLength);
|
|
515
|
+
if (retainedLength > 0) {
|
|
516
|
+
const handle = await fsp.open(target, "r");
|
|
517
|
+
try {
|
|
518
|
+
await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
|
|
519
|
+
} finally {
|
|
520
|
+
await handle.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
let tail = buffer.toString("utf8");
|
|
524
|
+
if (retainedLength < stat.size) {
|
|
525
|
+
const firstNewline = tail.indexOf(`
|
|
526
|
+
`);
|
|
527
|
+
tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
|
|
528
|
+
}
|
|
529
|
+
await atomicWriteFile(target, tail);
|
|
530
|
+
}
|
|
531
|
+
await fsp.appendFile(target, payload, { mode: 384 });
|
|
532
|
+
await fsp.chmod(target, 384);
|
|
533
|
+
} finally {
|
|
534
|
+
release();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
488
537
|
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
489
538
|
if (limit <= 0 || !fs.existsSync(target))
|
|
490
539
|
return [];
|
|
@@ -810,6 +859,13 @@ function getProviderCaps(provider) {
|
|
|
810
859
|
}
|
|
811
860
|
return DEFAULT_CAPS;
|
|
812
861
|
}
|
|
862
|
+
function safeContextPercent(totalTokens, contextWindow) {
|
|
863
|
+
if (!Number.isFinite(totalTokens) || !Number.isFinite(contextWindow))
|
|
864
|
+
return 0;
|
|
865
|
+
if ((totalTokens ?? 0) <= 0 || (contextWindow ?? 0) <= 0)
|
|
866
|
+
return 0;
|
|
867
|
+
return totalTokens / contextWindow * 100;
|
|
868
|
+
}
|
|
813
869
|
|
|
814
870
|
class TokenCalibrationStore {
|
|
815
871
|
maxEntries;
|
|
@@ -894,11 +950,14 @@ function makeTokenEstimator(provider, model, calibration = _fallbackCalibration)
|
|
|
894
950
|
}
|
|
895
951
|
|
|
896
952
|
// src/utils/type-guards.ts
|
|
953
|
+
function isRecord(value) {
|
|
954
|
+
return typeof value === "object" && value !== null;
|
|
955
|
+
}
|
|
897
956
|
function isTextBlock(c) {
|
|
898
|
-
return
|
|
957
|
+
return isRecord(c) && c.type === "text" && typeof c.text === "string";
|
|
899
958
|
}
|
|
900
959
|
function isToolCallBlock(c) {
|
|
901
|
-
return
|
|
960
|
+
return isRecord(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord(c.arguments);
|
|
902
961
|
}
|
|
903
962
|
function getToolCallNames(content) {
|
|
904
963
|
if (!Array.isArray(content))
|
|
@@ -1056,6 +1115,19 @@ function buildPathNeedles(filePath) {
|
|
|
1056
1115
|
}
|
|
1057
1116
|
return needles;
|
|
1058
1117
|
}
|
|
1118
|
+
function buildUniquePathNeedles(filePath, allPaths) {
|
|
1119
|
+
const normalized = allPaths.map(normalizePath);
|
|
1120
|
+
return buildPathNeedles(filePath).filter((needle) => {
|
|
1121
|
+
let owners = 0;
|
|
1122
|
+
for (const candidate of normalized) {
|
|
1123
|
+
if (candidate === needle || candidate.endsWith("/" + needle))
|
|
1124
|
+
owners++;
|
|
1125
|
+
if (owners > 1)
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
return owners === 1;
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1059
1131
|
function isKnownPathReference(ref, knownPaths) {
|
|
1060
1132
|
const normalizedRef = normalizePath(ref);
|
|
1061
1133
|
return knownPaths.some((path3) => {
|
|
@@ -1102,6 +1174,152 @@ function extractToolPath(args) {
|
|
|
1102
1174
|
}
|
|
1103
1175
|
return;
|
|
1104
1176
|
}
|
|
1177
|
+
function tokenizeShell(command) {
|
|
1178
|
+
const tokens = [];
|
|
1179
|
+
let word = "";
|
|
1180
|
+
let quote = null;
|
|
1181
|
+
const flush = () => {
|
|
1182
|
+
if (word)
|
|
1183
|
+
tokens.push({ kind: "word", value: word });
|
|
1184
|
+
word = "";
|
|
1185
|
+
};
|
|
1186
|
+
for (let index = 0;index < command.length; index++) {
|
|
1187
|
+
const char = command[index];
|
|
1188
|
+
if (char === "\\" && quote !== "'" && index + 1 < command.length) {
|
|
1189
|
+
word += command[++index];
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (char === "'" || char === '"') {
|
|
1193
|
+
if (!quote)
|
|
1194
|
+
quote = char;
|
|
1195
|
+
else if (quote === char)
|
|
1196
|
+
quote = null;
|
|
1197
|
+
else
|
|
1198
|
+
word += char;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (quote) {
|
|
1202
|
+
word += char;
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (/\s/.test(char)) {
|
|
1206
|
+
flush();
|
|
1207
|
+
if (char === `
|
|
1208
|
+
`)
|
|
1209
|
+
tokens.push({ kind: "separator", value: char });
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (char === ">" || char === ";" || char === "|" || char === "&" && command[index + 1] === "&") {
|
|
1213
|
+
flush();
|
|
1214
|
+
if (char === ">") {
|
|
1215
|
+
const append = command[index + 1] === ">";
|
|
1216
|
+
if (append)
|
|
1217
|
+
index++;
|
|
1218
|
+
tokens.push({ kind: "redirect", value: append ? ">>" : ">" });
|
|
1219
|
+
} else {
|
|
1220
|
+
const paired = char === "|" && command[index + 1] === "|" || char === "&" && command[index + 1] === "&";
|
|
1221
|
+
if (paired)
|
|
1222
|
+
index++;
|
|
1223
|
+
tokens.push({ kind: "separator", value: paired ? char + char : char });
|
|
1224
|
+
}
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
word += char;
|
|
1228
|
+
}
|
|
1229
|
+
flush();
|
|
1230
|
+
return tokens;
|
|
1231
|
+
}
|
|
1232
|
+
function literalShellPath(token) {
|
|
1233
|
+
if (!token || token.startsWith("-") || token === "/dev/null")
|
|
1234
|
+
return;
|
|
1235
|
+
if (/[\u0000$*?\[\]{}()<>|;&]/.test(token) || /^\d+$/.test(token))
|
|
1236
|
+
return;
|
|
1237
|
+
return token;
|
|
1238
|
+
}
|
|
1239
|
+
function shellOperands(words, start) {
|
|
1240
|
+
const operands = [];
|
|
1241
|
+
let options = true;
|
|
1242
|
+
for (let index = start;index < words.length; index++) {
|
|
1243
|
+
const word = words[index];
|
|
1244
|
+
if (options && word === "--") {
|
|
1245
|
+
options = false;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (options && word.startsWith("-"))
|
|
1249
|
+
continue;
|
|
1250
|
+
const target = literalShellPath(word);
|
|
1251
|
+
if (target)
|
|
1252
|
+
operands.push(target);
|
|
1253
|
+
}
|
|
1254
|
+
return operands;
|
|
1255
|
+
}
|
|
1256
|
+
function commandFileOperations(words) {
|
|
1257
|
+
const modified = [];
|
|
1258
|
+
const deleted = [];
|
|
1259
|
+
let commandIndex = 0;
|
|
1260
|
+
while (commandIndex < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[commandIndex]))
|
|
1261
|
+
commandIndex++;
|
|
1262
|
+
while (commandIndex < words.length) {
|
|
1263
|
+
const wrapper = words[commandIndex].split("/").pop()?.toLowerCase();
|
|
1264
|
+
if (wrapper !== "env" && wrapper !== "sudo" && wrapper !== "command" && wrapper !== "nohup")
|
|
1265
|
+
break;
|
|
1266
|
+
commandIndex++;
|
|
1267
|
+
while (commandIndex < words.length && words[commandIndex].startsWith("-"))
|
|
1268
|
+
commandIndex++;
|
|
1269
|
+
}
|
|
1270
|
+
const command = words[commandIndex]?.split("/").pop()?.toLowerCase();
|
|
1271
|
+
const operands = shellOperands(words, commandIndex + 1);
|
|
1272
|
+
if (!command || !operands.length)
|
|
1273
|
+
return { modified, deleted };
|
|
1274
|
+
if (command === "rm" || command === "unlink") {
|
|
1275
|
+
deleted.push(...operands);
|
|
1276
|
+
} else if (command === "touch" || command === "tee") {
|
|
1277
|
+
modified.push(...operands);
|
|
1278
|
+
} else if (command === "cp" || command === "install") {
|
|
1279
|
+
modified.push(operands[operands.length - 1]);
|
|
1280
|
+
} else if (command === "mv") {
|
|
1281
|
+
deleted.push(...operands.slice(0, -1));
|
|
1282
|
+
modified.push(operands[operands.length - 1]);
|
|
1283
|
+
} else if (command === "sed" && words.slice(commandIndex + 1).some((word) => /^-i|^--in-place/.test(word))) {
|
|
1284
|
+
modified.push(operands[operands.length - 1]);
|
|
1285
|
+
}
|
|
1286
|
+
return { modified, deleted };
|
|
1287
|
+
}
|
|
1288
|
+
function extractShellFileOperations(args) {
|
|
1289
|
+
const record = args && typeof args === "object" ? args : null;
|
|
1290
|
+
const command = record ? COMMAND_KEYS.map((key) => record[key]).find((value) => typeof value === "string") : undefined;
|
|
1291
|
+
if (!command)
|
|
1292
|
+
return { modified: [], deleted: [] };
|
|
1293
|
+
const tokens = tokenizeShell(command);
|
|
1294
|
+
const modified = [];
|
|
1295
|
+
const deleted = [];
|
|
1296
|
+
let words = [];
|
|
1297
|
+
const flushCommand = () => {
|
|
1298
|
+
const operations = commandFileOperations(words);
|
|
1299
|
+
modified.push(...operations.modified);
|
|
1300
|
+
deleted.push(...operations.deleted);
|
|
1301
|
+
words = [];
|
|
1302
|
+
};
|
|
1303
|
+
for (let index = 0;index < tokens.length; index++) {
|
|
1304
|
+
const token = tokens[index];
|
|
1305
|
+
if (token.kind === "separator") {
|
|
1306
|
+
flushCommand();
|
|
1307
|
+
} else if (token.kind === "redirect") {
|
|
1308
|
+
const target = tokens[index + 1]?.kind === "word" ? literalShellPath(tokens[index + 1].value) : undefined;
|
|
1309
|
+
if (target)
|
|
1310
|
+
modified.push(target);
|
|
1311
|
+
if (target)
|
|
1312
|
+
index++;
|
|
1313
|
+
} else {
|
|
1314
|
+
words.push(token.value);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
flushCommand();
|
|
1318
|
+
return {
|
|
1319
|
+
modified: Array.from(new Set(modified)),
|
|
1320
|
+
deleted: Array.from(new Set(deleted.filter((file) => !modified.includes(file))))
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1105
1323
|
function normalizeToolName(name) {
|
|
1106
1324
|
if (typeof name !== "string")
|
|
1107
1325
|
return "";
|
|
@@ -1276,6 +1494,7 @@ function parseSummary(markdown) {
|
|
|
1276
1494
|
let currentHeading = "";
|
|
1277
1495
|
let currentKind = "unknown";
|
|
1278
1496
|
let bodyLines = [];
|
|
1497
|
+
let fence = null;
|
|
1279
1498
|
let started = false;
|
|
1280
1499
|
const flush = () => {
|
|
1281
1500
|
if (!started)
|
|
@@ -1289,16 +1508,31 @@ function parseSummary(markdown) {
|
|
|
1289
1508
|
sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
|
|
1290
1509
|
};
|
|
1291
1510
|
for (const line of lines) {
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1511
|
+
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
1512
|
+
if (fenceMatch) {
|
|
1513
|
+
const marker = fenceMatch[1][0];
|
|
1514
|
+
const markerLength = fenceMatch[1].length;
|
|
1515
|
+
if (!fence) {
|
|
1516
|
+
fence = { marker, length: markerLength };
|
|
1517
|
+
} else if (marker === fence.marker && markerLength >= fence.length && !fenceMatch[2].trim()) {
|
|
1518
|
+
fence = null;
|
|
1519
|
+
}
|
|
1520
|
+
if (started)
|
|
1521
|
+
bodyLines.push(line);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (!fence) {
|
|
1525
|
+
const heading = line.match(HEADING_RE);
|
|
1526
|
+
if (heading) {
|
|
1527
|
+
const kind = classifyHeading(heading[2]);
|
|
1528
|
+
if (heading[1].length <= 2 || kind !== "unknown") {
|
|
1529
|
+
flush();
|
|
1530
|
+
currentHeading = "## " + heading[2].trim();
|
|
1531
|
+
currentKind = kind;
|
|
1532
|
+
bodyLines = [];
|
|
1533
|
+
started = true;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1302
1536
|
}
|
|
1303
1537
|
}
|
|
1304
1538
|
if (started)
|
|
@@ -1472,39 +1706,61 @@ function buildToolCallIndex(msgs) {
|
|
|
1472
1706
|
function trackFileOps(msgs, _tcIdx) {
|
|
1473
1707
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
1474
1708
|
const modMap = new Map;
|
|
1475
|
-
const
|
|
1476
|
-
const
|
|
1709
|
+
const readAt = new Map;
|
|
1710
|
+
const deletedAt = new Map;
|
|
1711
|
+
const referencedAt = new Map;
|
|
1477
1712
|
for (let i = 0;i < msgs.length; i++) {
|
|
1478
1713
|
const m = msgs[i];
|
|
1714
|
+
for (const ref of extractFileRefs((JSON.stringify(m.content) ?? "").replace(/\\[nrt]/g, " "))) {
|
|
1715
|
+
referencedAt.set(ref, i);
|
|
1716
|
+
}
|
|
1479
1717
|
if (m.role !== "toolResult" || m.isError)
|
|
1480
1718
|
continue;
|
|
1481
1719
|
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
1482
1720
|
if (!tc)
|
|
1483
1721
|
continue;
|
|
1722
|
+
const operation = classifyToolOperation(tc.arguments, tc.name);
|
|
1723
|
+
if (operation === "execute") {
|
|
1724
|
+
const resultText = extractText(m.content);
|
|
1725
|
+
if (hasCommandFailureSignal(resultText))
|
|
1726
|
+
continue;
|
|
1727
|
+
const shell = extractShellFileOperations(tc.arguments);
|
|
1728
|
+
for (const file of shell.modified) {
|
|
1729
|
+
const existing = modMap.get(file);
|
|
1730
|
+
modMap.set(file, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1731
|
+
deletedAt.delete(file);
|
|
1732
|
+
}
|
|
1733
|
+
for (const file of shell.deleted) {
|
|
1734
|
+
deletedAt.set(file, i);
|
|
1735
|
+
modMap.delete(file);
|
|
1736
|
+
readAt.delete(file);
|
|
1737
|
+
}
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1484
1740
|
const filePath = extractToolPath(tc.arguments);
|
|
1485
1741
|
if (!filePath)
|
|
1486
1742
|
continue;
|
|
1487
|
-
const operation = classifyToolOperation(tc.arguments, tc.name);
|
|
1488
1743
|
if (operation === "mutate") {
|
|
1489
1744
|
const resultText = extractText(m.content);
|
|
1490
1745
|
if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
|
|
1491
1746
|
const existing = modMap.get(filePath);
|
|
1492
1747
|
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
1493
|
-
|
|
1748
|
+
deletedAt.delete(filePath);
|
|
1494
1749
|
}
|
|
1495
1750
|
} else if (operation === "delete") {
|
|
1496
|
-
|
|
1751
|
+
deletedAt.set(filePath, i);
|
|
1497
1752
|
modMap.delete(filePath);
|
|
1498
|
-
|
|
1753
|
+
readAt.delete(filePath);
|
|
1499
1754
|
} else if (operation === "read" || operation === "search" || operation === "list") {
|
|
1500
|
-
|
|
1501
|
-
|
|
1755
|
+
readAt.set(filePath, i);
|
|
1756
|
+
deletedAt.delete(filePath);
|
|
1502
1757
|
}
|
|
1503
1758
|
}
|
|
1504
1759
|
return {
|
|
1505
1760
|
modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
|
|
1506
|
-
read: [...
|
|
1507
|
-
deleted: [...
|
|
1761
|
+
read: [...readAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
|
|
1762
|
+
deleted: [...deletedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
|
|
1763
|
+
referenced: [...referencedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file)
|
|
1508
1764
|
};
|
|
1509
1765
|
}
|
|
1510
1766
|
function isBenignSearchResult(tc, result) {
|
|
@@ -1779,6 +2035,29 @@ function extractMainGoal(msgs) {
|
|
|
1779
2035
|
}
|
|
1780
2036
|
return null;
|
|
1781
2037
|
}
|
|
2038
|
+
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;
|
|
2039
|
+
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;
|
|
2040
|
+
var FOLLOWUP_TOKEN_STOP = {
|
|
2041
|
+
next: true,
|
|
2042
|
+
step: true,
|
|
2043
|
+
thing: true,
|
|
2044
|
+
todo: true,
|
|
2045
|
+
action: true,
|
|
2046
|
+
item: true,
|
|
2047
|
+
follow: true,
|
|
2048
|
+
still: true,
|
|
2049
|
+
need: true,
|
|
2050
|
+
have: true,
|
|
2051
|
+
gotta: true,
|
|
2052
|
+
eklenecek: true,
|
|
2053
|
+
duzeltilecek: true,
|
|
2054
|
+
d\u{fc}zeltilecek: true,
|
|
2055
|
+
gerekiyor: true,
|
|
2056
|
+
yapalim: true,
|
|
2057
|
+
yapal\u{131}m: true,
|
|
2058
|
+
kaldi: true,
|
|
2059
|
+
kald\u{131}: true
|
|
2060
|
+
};
|
|
1782
2061
|
function extractOpenLoops(msgs, extraction) {
|
|
1783
2062
|
const loops = [];
|
|
1784
2063
|
let loopId = 0;
|
|
@@ -1859,6 +2138,29 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
1859
2138
|
});
|
|
1860
2139
|
}
|
|
1861
2140
|
}
|
|
2141
|
+
for (const loop of loops) {
|
|
2142
|
+
if (loop.type !== "follow-up" || loop.sourceIndex == null)
|
|
2143
|
+
continue;
|
|
2144
|
+
const taskTokens = (loop.summary.toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) ?? []).filter((token) => !FOLLOWUP_TOKEN_STOP[token]);
|
|
2145
|
+
if (!taskTokens.length)
|
|
2146
|
+
continue;
|
|
2147
|
+
const end = Math.min(msgs.length, loop.sourceIndex + 50);
|
|
2148
|
+
for (let index = loop.sourceIndex + 1;index < end; index++) {
|
|
2149
|
+
const message = msgs[index];
|
|
2150
|
+
if (message?.role === "user")
|
|
2151
|
+
break;
|
|
2152
|
+
if (message?.role !== "assistant")
|
|
2153
|
+
continue;
|
|
2154
|
+
const response = extractText(message.content);
|
|
2155
|
+
const normalized = response.toLowerCase();
|
|
2156
|
+
if (!FOLLOWUP_COMPLETION_RE.test(response) || NEGATED_COMPLETION_RE.test(response))
|
|
2157
|
+
continue;
|
|
2158
|
+
if (taskTokens.some((token) => normalized.includes(token))) {
|
|
2159
|
+
loop.status = "resolved";
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1862
2164
|
return loops;
|
|
1863
2165
|
}
|
|
1864
2166
|
function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
@@ -1877,20 +2179,24 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1877
2179
|
const errors = recent(allErrors, EXTRACTION_LIMITS.ERRORS);
|
|
1878
2180
|
const decisions = recent(allDecisions, EXTRACTION_LIMITS.DECISIONS);
|
|
1879
2181
|
const constraints = recent(allConstraints, EXTRACTION_LIMITS.CONSTRAINTS);
|
|
2182
|
+
const boundedTopics = recent(topics, EXTRACTION_LIMITS.TOPICS);
|
|
1880
2183
|
const timeline = recent(allTimeline, EXTRACTION_LIMITS.TIMELINE);
|
|
1881
2184
|
const mediaAttachments = recent(allMediaAttachments, EXTRACTION_LIMITS.MEDIA_ATTACHMENTS);
|
|
2185
|
+
const allReferencedFiles = tracked.referenced;
|
|
2186
|
+
const referencedFiles = recent(allReferencedFiles, EXTRACTION_LIMITS.REFERENCED_FILES);
|
|
1882
2187
|
const overflow = {
|
|
1883
2188
|
modifiedFiles: tracked.modified.length - modifiedFiles.length,
|
|
2189
|
+
referencedFiles: allReferencedFiles.length - referencedFiles.length,
|
|
1884
2190
|
readFiles: tracked.read.length - readFiles.length,
|
|
1885
2191
|
deletedFiles: tracked.deleted.length - deletedFiles.length,
|
|
1886
2192
|
errors: allErrors.length - errors.length,
|
|
1887
2193
|
decisions: allDecisions.length - decisions.length,
|
|
1888
2194
|
constraints: allConstraints.length - constraints.length,
|
|
2195
|
+
topics: topics.length - boundedTopics.length,
|
|
1889
2196
|
timeline: allTimeline.length - timeline.length,
|
|
1890
2197
|
mediaAttachments: allMediaAttachments.length - mediaAttachments.length
|
|
1891
2198
|
};
|
|
1892
2199
|
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
2200
|
const mainGoal = extractMainGoal(msgs);
|
|
1895
2201
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
1896
2202
|
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
@@ -1902,7 +2208,7 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
1902
2208
|
errors,
|
|
1903
2209
|
decisions,
|
|
1904
2210
|
constraints,
|
|
1905
|
-
topics,
|
|
2211
|
+
topics: boundedTopics,
|
|
1906
2212
|
timeline,
|
|
1907
2213
|
mediaAttachments,
|
|
1908
2214
|
mainGoal,
|
|
@@ -2176,39 +2482,34 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
|
|
|
2176
2482
|
return candidates;
|
|
2177
2483
|
}
|
|
2178
2484
|
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
|
-
}
|
|
2485
|
+
for (const block of blocks) {
|
|
2486
|
+
if (!isRecord(block) || block.type !== "toolCall")
|
|
2487
|
+
continue;
|
|
2488
|
+
if (typeof block.id === "string")
|
|
2489
|
+
out.set(block.id, msgIndex);
|
|
2490
|
+
const args = block.arguments;
|
|
2491
|
+
if (block.name !== "multi_tool_use.parallel" || !isRecord(args) || !Array.isArray(args.tool_uses))
|
|
2492
|
+
continue;
|
|
2493
|
+
for (const nested of args.tool_uses) {
|
|
2494
|
+
if (isRecord(nested) && typeof nested.id === "string")
|
|
2495
|
+
out.set(nested.id, msgIndex);
|
|
2194
2496
|
}
|
|
2195
2497
|
}
|
|
2196
2498
|
}
|
|
2197
|
-
function
|
|
2499
|
+
function buildToolCallBoundaryIndex(msgs) {
|
|
2198
2500
|
const map = new Map;
|
|
2199
2501
|
for (let i = 0;i < msgs.length; i++) {
|
|
2200
|
-
const
|
|
2201
|
-
if (
|
|
2502
|
+
const message = msgs[i].message;
|
|
2503
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
2202
2504
|
continue;
|
|
2203
|
-
const blocks = Array.isArray(
|
|
2505
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
2204
2506
|
collectToolCallIds(blocks, i, map);
|
|
2205
2507
|
}
|
|
2206
2508
|
return map;
|
|
2207
2509
|
}
|
|
2208
|
-
function guardToolCallBoundary(msgs, keepFrom) {
|
|
2510
|
+
function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryIndex(msgs)) {
|
|
2209
2511
|
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
2210
2512
|
return keepFrom;
|
|
2211
|
-
const tcMap = toolCallIndexMap(msgs);
|
|
2212
2513
|
let adjusted = keepFrom;
|
|
2213
2514
|
let changed = true;
|
|
2214
2515
|
const MAX_ITER = msgs.length + 1;
|
|
@@ -2220,10 +2521,10 @@ function guardToolCallBoundary(msgs, keepFrom) {
|
|
|
2220
2521
|
}
|
|
2221
2522
|
changed = false;
|
|
2222
2523
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
2223
|
-
const
|
|
2224
|
-
if (
|
|
2524
|
+
const message = msgs[i].message;
|
|
2525
|
+
if (!isRecord(message) || message.role !== "toolResult")
|
|
2225
2526
|
continue;
|
|
2226
|
-
const tcId =
|
|
2527
|
+
const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
2227
2528
|
if (!tcId)
|
|
2228
2529
|
continue;
|
|
2229
2530
|
const tcIdx = tcMap.get(tcId);
|
|
@@ -2236,18 +2537,17 @@ function guardToolCallBoundary(msgs, keepFrom) {
|
|
|
2236
2537
|
}
|
|
2237
2538
|
return Math.max(0, Math.min(adjusted, msgs.length));
|
|
2238
2539
|
}
|
|
2239
|
-
function advancePastToolCallBoundary(msgs, keepFrom) {
|
|
2540
|
+
function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryIndex(msgs)) {
|
|
2240
2541
|
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
2241
2542
|
return keepFrom;
|
|
2242
|
-
const tcMap = toolCallIndexMap(msgs);
|
|
2243
2543
|
let adjusted = keepFrom;
|
|
2244
2544
|
for (let iter = 0;iter <= msgs.length; iter++) {
|
|
2245
2545
|
let next = adjusted;
|
|
2246
2546
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
2247
|
-
const
|
|
2248
|
-
if (
|
|
2547
|
+
const message = msgs[i].message;
|
|
2548
|
+
if (!isRecord(message) || message.role !== "toolResult")
|
|
2249
2549
|
continue;
|
|
2250
|
-
const tcIdx = typeof
|
|
2550
|
+
const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
|
|
2251
2551
|
if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
|
|
2252
2552
|
next = i + 1;
|
|
2253
2553
|
break;
|
|
@@ -2916,30 +3216,41 @@ function mergeFindings(target, findings) {
|
|
|
2916
3216
|
for (const finding of findings)
|
|
2917
3217
|
target.set(finding.kind, (target.get(finding.kind) ?? 0) + finding.count);
|
|
2918
3218
|
}
|
|
2919
|
-
var SECRET_KEY_NAMES =
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3219
|
+
var SECRET_KEY_NAMES = {
|
|
3220
|
+
api_key: true,
|
|
3221
|
+
apikey: true,
|
|
3222
|
+
access_token: true,
|
|
3223
|
+
auth_token: true,
|
|
3224
|
+
authorization: true,
|
|
3225
|
+
password: true,
|
|
3226
|
+
passwd: true,
|
|
3227
|
+
secret: true,
|
|
3228
|
+
secret_key: true,
|
|
3229
|
+
secret_access_key: true,
|
|
3230
|
+
client_secret: true,
|
|
3231
|
+
private_key: true,
|
|
3232
|
+
database_url: true,
|
|
3233
|
+
connection_string: true,
|
|
3234
|
+
token: true,
|
|
3235
|
+
refresh_token: true,
|
|
3236
|
+
session_token: true,
|
|
3237
|
+
credential: true,
|
|
3238
|
+
credentials: true,
|
|
3239
|
+
cookie: true,
|
|
3240
|
+
set_cookie: true,
|
|
3241
|
+
otp: true,
|
|
3242
|
+
one_time_password: true,
|
|
3243
|
+
pin: true,
|
|
3244
|
+
passcode: true
|
|
3245
|
+
};
|
|
2935
3246
|
function normalizeObjectKey(key) {
|
|
2936
3247
|
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2937
3248
|
}
|
|
2938
3249
|
function isSecretBearingKey(key) {
|
|
2939
3250
|
const normalized = normalizeObjectKey(key);
|
|
2940
|
-
if (SECRET_KEY_NAMES
|
|
3251
|
+
if (SECRET_KEY_NAMES[normalized])
|
|
2941
3252
|
return true;
|
|
2942
|
-
return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key)(?:_|$)/.test(normalized);
|
|
3253
|
+
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
3254
|
}
|
|
2944
3255
|
|
|
2945
3256
|
class SecretScrubber {
|
|
@@ -2995,7 +3306,8 @@ class SecretScrubber {
|
|
|
2995
3306
|
const output = {};
|
|
2996
3307
|
seen.set(value2, output);
|
|
2997
3308
|
for (const [key, item] of Object.entries(value2)) {
|
|
2998
|
-
|
|
3309
|
+
const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
|
|
3310
|
+
if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
|
|
2999
3311
|
output[key] = "[REDACTED:credential]";
|
|
3000
3312
|
recordCredential();
|
|
3001
3313
|
} else {
|
|
@@ -3466,76 +3778,125 @@ function reconcileCachedErrors(errors, deltaMessages, deltaToolCalls, baseMsgCou
|
|
|
3466
3778
|
return { ...error2, retryAttempted, resolved };
|
|
3467
3779
|
});
|
|
3468
3780
|
}
|
|
3781
|
+
function boundedTail(items, limit) {
|
|
3782
|
+
const dropped = Math.max(0, items.length - limit);
|
|
3783
|
+
return { values: dropped ? items.slice(-limit) : items, dropped };
|
|
3784
|
+
}
|
|
3785
|
+
function recentUnique(items, limit) {
|
|
3786
|
+
const seen = new Set;
|
|
3787
|
+
const newestFirst = [];
|
|
3788
|
+
for (let index = items.length - 1;index >= 0; index--) {
|
|
3789
|
+
const item = items[index];
|
|
3790
|
+
if (seen.has(item))
|
|
3791
|
+
continue;
|
|
3792
|
+
seen.add(item);
|
|
3793
|
+
if (newestFirst.length < limit)
|
|
3794
|
+
newestFirst.push(item);
|
|
3795
|
+
}
|
|
3796
|
+
return {
|
|
3797
|
+
values: newestFirst.reverse(),
|
|
3798
|
+
dropped: Math.max(0, seen.size - limit)
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3469
3801
|
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:
|
|
3802
|
+
const offsetErrors = delta.errors.map((error2) => ({ ...error2, index: error2.index + baseMsgCount }));
|
|
3803
|
+
const offsetDecisions = delta.decisions.map((decision) => ({ ...decision, index: decision.index + baseMsgCount }));
|
|
3804
|
+
const offsetConstraints = delta.constraints.map((constraint) => ({ ...constraint, index: constraint.index + baseMsgCount }));
|
|
3805
|
+
const offsetTopics = delta.topics.map((topic) => ({
|
|
3806
|
+
...topic,
|
|
3807
|
+
startIndex: topic.startIndex + baseMsgCount,
|
|
3808
|
+
endIndex: topic.endIndex + baseMsgCount
|
|
3477
3809
|
}));
|
|
3478
|
-
const offsetTimeline = delta.timeline.map((
|
|
3479
|
-
const offsetModifiedFiles = delta.modifiedFiles.map((
|
|
3480
|
-
...
|
|
3481
|
-
lastModifiedIndex:
|
|
3810
|
+
const offsetTimeline = delta.timeline.map((event) => ({ ...event, index: event.index + baseMsgCount }));
|
|
3811
|
+
const offsetModifiedFiles = delta.modifiedFiles.map((file) => ({
|
|
3812
|
+
...file,
|
|
3813
|
+
lastModifiedIndex: file.lastModifiedIndex + baseMsgCount
|
|
3814
|
+
}));
|
|
3815
|
+
const offsetMedia = (delta.mediaAttachments ?? []).map((attachment) => ({
|
|
3816
|
+
...attachment,
|
|
3817
|
+
index: attachment.index + baseMsgCount
|
|
3482
3818
|
}));
|
|
3483
|
-
const offsetMedia = (delta.mediaAttachments ?? []).map((a) => ({ ...a, index: a.index + baseMsgCount }));
|
|
3484
3819
|
const modified = new Map(base.modifiedFiles.map((file) => [file.path, { ...file }]));
|
|
3485
3820
|
for (const file of offsetModifiedFiles) {
|
|
3486
3821
|
const previous = modified.get(file.path);
|
|
3487
|
-
modified.set(file.path, previous ? {
|
|
3822
|
+
modified.set(file.path, previous ? {
|
|
3823
|
+
...file,
|
|
3824
|
+
toolCalls: previous.toolCalls + file.toolCalls,
|
|
3825
|
+
lastModifiedIndex: Math.max(previous.lastModifiedIndex, file.lastModifiedIndex)
|
|
3826
|
+
} : file);
|
|
3488
3827
|
}
|
|
3489
3828
|
const deltaPresent = new Set([...offsetModifiedFiles.map((file) => file.path), ...delta.readFiles]);
|
|
3490
3829
|
const deltaDeleted = new Set(delta.deletedFiles);
|
|
3491
3830
|
for (const file of deltaDeleted)
|
|
3492
3831
|
modified.delete(file);
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
deletedFiles.delete(file);
|
|
3832
|
+
const modifiedFiles = boundedTail([...modified.values()].sort((a, b) => a.lastModifiedIndex - b.lastModifiedIndex), EXTRACTION_LIMITS.MODIFIED_FILES);
|
|
3833
|
+
const readFiles = recentUnique([...base.readFiles, ...delta.readFiles].filter((file) => !deltaDeleted.has(file)), EXTRACTION_LIMITS.READ_FILES);
|
|
3834
|
+
const deletedFiles = recentUnique([...base.deletedFiles, ...delta.deletedFiles].filter((file) => !deltaPresent.has(file)), EXTRACTION_LIMITS.DELETED_FILES);
|
|
3835
|
+
const referencedFiles = recentUnique([...base.referencedFiles ?? [], ...delta.referencedFiles ?? []], EXTRACTION_LIMITS.REFERENCED_FILES);
|
|
3836
|
+
const mediaAttachments = boundedTail([...base.mediaAttachments ?? [], ...offsetMedia], EXTRACTION_LIMITS.MEDIA_ATTACHMENTS);
|
|
3499
3837
|
const reconciledBaseErrors = reconcileCachedErrors(base.errors, deltaMessages, deltaToolCalls, baseMsgCount);
|
|
3500
|
-
const
|
|
3838
|
+
const errors = boundedTail([...reconciledBaseErrors, ...offsetErrors].filter((error2) => !isTransientToolDiagnostic(error2.message)), EXTRACTION_LIMITS.ERRORS);
|
|
3839
|
+
const decisions = boundedTail([...base.decisions, ...offsetDecisions], EXTRACTION_LIMITS.DECISIONS);
|
|
3840
|
+
const constraints = boundedTail([...base.constraints, ...offsetConstraints], EXTRACTION_LIMITS.CONSTRAINTS);
|
|
3841
|
+
const topics = boundedTail([...base.topics, ...offsetTopics], EXTRACTION_LIMITS.TOPICS);
|
|
3842
|
+
const timeline = boundedTail([...base.timeline, ...offsetTimeline], EXTRACTION_LIMITS.TIMELINE);
|
|
3843
|
+
const dropped = {
|
|
3844
|
+
modifiedFiles: modifiedFiles.dropped,
|
|
3845
|
+
referencedFiles: referencedFiles.dropped,
|
|
3846
|
+
readFiles: readFiles.dropped,
|
|
3847
|
+
deletedFiles: deletedFiles.dropped,
|
|
3848
|
+
errors: errors.dropped,
|
|
3849
|
+
decisions: decisions.dropped,
|
|
3850
|
+
constraints: constraints.dropped,
|
|
3851
|
+
topics: topics.dropped,
|
|
3852
|
+
timeline: timeline.dropped,
|
|
3853
|
+
mediaAttachments: mediaAttachments.dropped
|
|
3854
|
+
};
|
|
3855
|
+
const evidenceOverflow = {};
|
|
3856
|
+
for (const key of Object.keys(dropped)) {
|
|
3857
|
+
const total = (base.evidenceOverflow?.[key] ?? 0) + (delta.evidenceOverflow?.[key] ?? 0) + (dropped[key] ?? 0);
|
|
3858
|
+
if (total > 0)
|
|
3859
|
+
Object.assign(evidenceOverflow, { [key]: total });
|
|
3860
|
+
}
|
|
3501
3861
|
return {
|
|
3502
|
-
modifiedFiles:
|
|
3503
|
-
readFiles:
|
|
3504
|
-
deletedFiles:
|
|
3505
|
-
referencedFiles:
|
|
3506
|
-
mediaAttachments:
|
|
3507
|
-
errors:
|
|
3508
|
-
decisions:
|
|
3509
|
-
constraints:
|
|
3510
|
-
topics:
|
|
3511
|
-
timeline:
|
|
3862
|
+
modifiedFiles: modifiedFiles.values,
|
|
3863
|
+
readFiles: readFiles.values,
|
|
3864
|
+
deletedFiles: deletedFiles.values,
|
|
3865
|
+
referencedFiles: referencedFiles.values,
|
|
3866
|
+
mediaAttachments: mediaAttachments.values,
|
|
3867
|
+
errors: errors.values,
|
|
3868
|
+
decisions: decisions.values,
|
|
3869
|
+
constraints: constraints.values,
|
|
3870
|
+
topics: topics.values,
|
|
3871
|
+
timeline: timeline.values,
|
|
3512
3872
|
mainGoal: delta.mainGoal ?? base.mainGoal,
|
|
3513
3873
|
lastUserMessages: [...base.lastUserMessages, ...delta.lastUserMessages].slice(-5),
|
|
3514
|
-
lastErrors:
|
|
3515
|
-
messageCount: baseMsgCount + delta.messageCount
|
|
3874
|
+
lastErrors: errors.values.filter((error2) => !error2.resolved).map((error2) => error2.message).slice(-3),
|
|
3875
|
+
messageCount: baseMsgCount + delta.messageCount,
|
|
3876
|
+
...Object.keys(evidenceOverflow).length ? { evidenceOverflow } : {}
|
|
3516
3877
|
};
|
|
3517
3878
|
}
|
|
3518
|
-
function appendMetricsEntry(entry) {
|
|
3879
|
+
async function appendMetricsEntry(entry) {
|
|
3519
3880
|
const logPath = metricsLogFile();
|
|
3520
|
-
|
|
3881
|
+
await appendLineLockedAsync(logPath, JSON.stringify(entry), RUNTIME_LOG_MAX_BYTES);
|
|
3521
3882
|
}
|
|
3522
|
-
function appendMetricsSnapshot(sessionId, snapshot) {
|
|
3883
|
+
async function appendMetricsSnapshot(sessionId, snapshot) {
|
|
3523
3884
|
try {
|
|
3524
|
-
appendMetricsEntry({ ts: new Date().toISOString(), sessionId, ...snapshot });
|
|
3525
|
-
} catch (
|
|
3526
|
-
warn("appendMetricsSnapshot failed",
|
|
3885
|
+
await appendMetricsEntry({ ts: new Date().toISOString(), sessionId, ...snapshot });
|
|
3886
|
+
} catch (error2) {
|
|
3887
|
+
warn("appendMetricsSnapshot failed", error2);
|
|
3527
3888
|
}
|
|
3528
3889
|
}
|
|
3529
|
-
function appendMetricsLog(sessionId, extra, services) {
|
|
3890
|
+
async function appendMetricsLog(sessionId, extra, services) {
|
|
3530
3891
|
try {
|
|
3531
|
-
appendMetricsEntry({
|
|
3892
|
+
await appendMetricsEntry({
|
|
3532
3893
|
ts: new Date().toISOString(),
|
|
3533
3894
|
sessionId,
|
|
3534
3895
|
...getMetricsSummary(services),
|
|
3535
3896
|
...extra
|
|
3536
3897
|
});
|
|
3537
|
-
} catch (
|
|
3538
|
-
warn("appendMetricsLog failed",
|
|
3898
|
+
} catch (error2) {
|
|
3899
|
+
warn("appendMetricsLog failed", error2);
|
|
3539
3900
|
}
|
|
3540
3901
|
}
|
|
3541
3902
|
function readMetricsLog(limit = 100) {
|
|
@@ -3550,8 +3911,8 @@ function readMetricsLog(limit = 100) {
|
|
|
3550
3911
|
const fd = fs4.openSync(logPath, "r");
|
|
3551
3912
|
try {
|
|
3552
3913
|
const buf = Buffer.alloc(wantBytes);
|
|
3553
|
-
fs4.readSync(fd, buf, 0, wantBytes, startPos);
|
|
3554
|
-
let text = buf.toString("utf8");
|
|
3914
|
+
const bytesRead = fs4.readSync(fd, buf, 0, wantBytes, startPos);
|
|
3915
|
+
let text = buf.subarray(0, bytesRead).toString("utf8");
|
|
3555
3916
|
if (startPos > 0) {
|
|
3556
3917
|
const nl = text.indexOf(`
|
|
3557
3918
|
`);
|
|
@@ -4439,7 +4800,20 @@ function acquireFileLease(file, staleMs) {
|
|
|
4439
4800
|
} finally {
|
|
4440
4801
|
fs5.closeSync(fd);
|
|
4441
4802
|
}
|
|
4442
|
-
|
|
4803
|
+
const lease = { file, token };
|
|
4804
|
+
lease.heartbeat = setInterval(() => {
|
|
4805
|
+
if (readLease(file)?.token !== token) {
|
|
4806
|
+
if (lease.heartbeat)
|
|
4807
|
+
clearInterval(lease.heartbeat);
|
|
4808
|
+
lease.heartbeat = undefined;
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
try {
|
|
4812
|
+
fs5.utimesSync(file, new Date, new Date);
|
|
4813
|
+
} catch {}
|
|
4814
|
+
}, Math.max(1e4, Math.floor(staleMs / 3)));
|
|
4815
|
+
lease.heartbeat.unref();
|
|
4816
|
+
return lease;
|
|
4443
4817
|
} catch (error2) {
|
|
4444
4818
|
if (error2.code !== "EEXIST")
|
|
4445
4819
|
throw error2;
|
|
@@ -4450,16 +4824,29 @@ function acquireFileLease(file, staleMs) {
|
|
|
4450
4824
|
if (first)
|
|
4451
4825
|
return first;
|
|
4452
4826
|
const current = readLease(file);
|
|
4453
|
-
let
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
return null;
|
|
4459
|
-
}
|
|
4827
|
+
let observedStat;
|
|
4828
|
+
try {
|
|
4829
|
+
observedStat = fs5.statSync(file);
|
|
4830
|
+
} catch {
|
|
4831
|
+
return null;
|
|
4460
4832
|
}
|
|
4833
|
+
const observedAt = Math.max(Number(current?.createdAt ?? 0), observedStat.mtimeMs);
|
|
4461
4834
|
const age = Date.now() - observedAt;
|
|
4462
|
-
|
|
4835
|
+
const livePidCeiling = Math.max(ONE_HOUR_MS, staleMs * 4);
|
|
4836
|
+
if (age <= staleMs || current?.pid && processAlive(current.pid) && age <= livePidCeiling)
|
|
4837
|
+
return null;
|
|
4838
|
+
const latest = readLease(file);
|
|
4839
|
+
let latestStat;
|
|
4840
|
+
try {
|
|
4841
|
+
latestStat = fs5.statSync(file);
|
|
4842
|
+
} catch {
|
|
4843
|
+
return null;
|
|
4844
|
+
}
|
|
4845
|
+
const latestAt = Math.max(Number(latest?.createdAt ?? 0), latestStat.mtimeMs);
|
|
4846
|
+
const latestAge = Date.now() - latestAt;
|
|
4847
|
+
if (latestAge <= staleMs || latest?.pid && processAlive(latest.pid) && latestAge <= livePidCeiling)
|
|
4848
|
+
return null;
|
|
4849
|
+
if (current?.token ? latest?.token !== current.token : latestStat.dev !== observedStat.dev || latestStat.ino !== observedStat.ino || latestStat.size !== observedStat.size || latestStat.mtimeMs !== observedStat.mtimeMs)
|
|
4463
4850
|
return null;
|
|
4464
4851
|
try {
|
|
4465
4852
|
fs5.unlinkSync(file);
|
|
@@ -4471,6 +4858,8 @@ function acquireFileLease(file, staleMs) {
|
|
|
4471
4858
|
function releaseFileLease(lease) {
|
|
4472
4859
|
if (!lease)
|
|
4473
4860
|
return;
|
|
4861
|
+
clearInterval(lease.heartbeat);
|
|
4862
|
+
lease.heartbeat = undefined;
|
|
4474
4863
|
const current = readLease(lease.file);
|
|
4475
4864
|
if (current?.token !== lease.token)
|
|
4476
4865
|
return;
|
|
@@ -5221,7 +5610,12 @@ function planCompactionWindow(input) {
|
|
|
5221
5610
|
overflowedContext,
|
|
5222
5611
|
finalSummaryAllowanceTokens
|
|
5223
5612
|
} = input;
|
|
5224
|
-
const
|
|
5613
|
+
const tokenPrefix = new Array(messageTokens.length + 1);
|
|
5614
|
+
tokenPrefix[0] = 0;
|
|
5615
|
+
for (let index = 0;index < messageTokens.length; index++) {
|
|
5616
|
+
tokenPrefix[index + 1] = tokenPrefix[index] + messageTokens[index];
|
|
5617
|
+
}
|
|
5618
|
+
const allMessageTokens = tokenPrefix[messageTokens.length];
|
|
5225
5619
|
const messageScale = totalTokens > 0 && allMessageTokens > totalTokens ? totalTokens / allMessageTokens : 1;
|
|
5226
5620
|
const fixedContextTokens = Math.max(0, totalTokens - allMessageTokens);
|
|
5227
5621
|
const adaptiveKeepTokens = modelContextWindow ? Math.min(profileCfg.keepRecentTokens * 2, Math.max(profileCfg.keepRecentTokens, modelContextWindow * 0.04)) : profileCfg.keepRecentTokens;
|
|
@@ -5244,39 +5638,50 @@ function planCompactionWindow(input) {
|
|
|
5244
5638
|
keepFrom = i;
|
|
5245
5639
|
}
|
|
5246
5640
|
const relaxedSoftBoundaries = [];
|
|
5247
|
-
const retainedAt = (from) => Math.round(
|
|
5641
|
+
const retainedAt = (from) => Math.round((allMessageTokens - tokenPrefix[from]) * messageScale);
|
|
5642
|
+
const toolCallIndex = buildToolCallBoundaryIndex(msgs);
|
|
5248
5643
|
const effectiveRetentionCeiling = Math.max(retentionCeiling, retainedAt(keepFrom));
|
|
5249
5644
|
let hardBoundaryAdjusted = false;
|
|
5250
5645
|
const trySoftBoundary = (kind, candidate) => {
|
|
5251
5646
|
if (candidate === undefined || candidate >= keepFrom)
|
|
5252
5647
|
return;
|
|
5253
|
-
const guarded = guardToolCallBoundary(msgs, candidate);
|
|
5648
|
+
const guarded = guardToolCallBoundary(msgs, candidate, toolCallIndex);
|
|
5254
5649
|
if (retainedAt(guarded) <= effectiveRetentionCeiling) {
|
|
5255
5650
|
keepFrom = guarded;
|
|
5256
5651
|
hardBoundaryAdjusted ||= guarded !== candidate;
|
|
5257
5652
|
} else
|
|
5258
5653
|
relaxedSoftBoundaries.push(kind);
|
|
5259
5654
|
};
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5655
|
+
let userOrdinal = 0;
|
|
5656
|
+
let protectedUserIndex;
|
|
5657
|
+
for (let index = msgs.length - 1;index >= 0; index--) {
|
|
5658
|
+
const message = msgs[index].message;
|
|
5659
|
+
if (!isRecord(message) || message.role !== "user")
|
|
5660
|
+
continue;
|
|
5661
|
+
userOrdinal++;
|
|
5662
|
+
protectedUserIndex = index;
|
|
5663
|
+
if (userOrdinal === 2)
|
|
5664
|
+
break;
|
|
5665
|
+
}
|
|
5666
|
+
trySoftBoundary("recent-user-turn", protectedUserIndex);
|
|
5263
5667
|
const anchor = smartKeepBoundaryCandidates(msgs, keepFrom, branch).find((candidate) => candidate.kind === "anchor");
|
|
5264
5668
|
trySoftBoundary("anchor", anchor?.keepFrom);
|
|
5265
5669
|
const topical = smartKeepBoundaryCandidates(msgs, keepFrom).find((candidate) => candidate.kind === "topical");
|
|
5266
5670
|
trySoftBoundary("topical", topical?.keepFrom);
|
|
5267
5671
|
const boundaryBeforeHardGuard = keepFrom;
|
|
5268
|
-
const backwardBoundary = guardToolCallBoundary(msgs, keepFrom);
|
|
5269
|
-
const forwardBoundary = retainedAt(backwardBoundary) > effectiveRetentionCeiling ? advancePastToolCallBoundary(msgs, keepFrom) : keepFrom;
|
|
5672
|
+
const backwardBoundary = guardToolCallBoundary(msgs, keepFrom, toolCallIndex);
|
|
5673
|
+
const forwardBoundary = retainedAt(backwardBoundary) > effectiveRetentionCeiling ? advancePastToolCallBoundary(msgs, keepFrom, toolCallIndex) : keepFrom;
|
|
5270
5674
|
keepFrom = forwardBoundary > keepFrom && forwardBoundary < msgs.length && retainedAt(forwardBoundary) <= effectiveRetentionCeiling ? forwardBoundary : backwardBoundary;
|
|
5271
5675
|
hardBoundaryAdjusted ||= keepFrom !== boundaryBeforeHardGuard;
|
|
5272
|
-
const compactTokens = Math.round(
|
|
5676
|
+
const compactTokens = Math.round(tokenPrefix[keepFrom] * messageScale);
|
|
5273
5677
|
const retainedTokens = retainedAt(keepFrom);
|
|
5274
5678
|
const projectedAfterTokens = fixedContextTokens + retainedTokens + finalSummaryAllowance;
|
|
5275
5679
|
const projectedSavedTokens = Math.max(0, totalTokens - projectedAfterTokens);
|
|
5276
5680
|
const projectedYield = totalTokens > 0 ? projectedSavedTokens / totalTokens : 0;
|
|
5277
5681
|
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
|
|
5278
5682
|
let reason = "viable";
|
|
5279
|
-
|
|
5683
|
+
const firstKeptMessage = msgs[keepFrom]?.message;
|
|
5684
|
+
if (isRecord(firstKeptMessage) && firstKeptMessage.role === "toolResult")
|
|
5280
5685
|
reason = "unsafe-tool-boundary";
|
|
5281
5686
|
else if (keepFrom <= 0)
|
|
5282
5687
|
reason = "no-eligible-prefix";
|
|
@@ -5315,7 +5720,8 @@ function resolveCompactionWindow(rc) {
|
|
|
5315
5720
|
return null;
|
|
5316
5721
|
}
|
|
5317
5722
|
const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
|
|
5318
|
-
const
|
|
5723
|
+
const modelContextWindow = rc.ctx.model?.contextWindow;
|
|
5724
|
+
const overflowedContext = !!rc.flags.overflowRecovery || Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 && totalTokens > modelContextWindow;
|
|
5319
5725
|
const plan = planCompactionWindow({
|
|
5320
5726
|
msgs,
|
|
5321
5727
|
branch,
|
|
@@ -5339,7 +5745,7 @@ function resolveCompactionWindow(rc) {
|
|
|
5339
5745
|
if (overflowedContext && plan.relaxedSoftBoundaries.length) {
|
|
5340
5746
|
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
5747
|
}
|
|
5342
|
-
const contextPercent =
|
|
5748
|
+
const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
|
|
5343
5749
|
if (rc.flags.force && rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
|
|
5344
5750
|
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
5751
|
}
|
|
@@ -5394,8 +5800,8 @@ function prepareManualPreflightContext(ctx, summaryModel, tokenCalibration) {
|
|
|
5394
5800
|
const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
|
|
5395
5801
|
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
5396
5802
|
const modelContextWindow = ctx.model?.contextWindow;
|
|
5397
|
-
const contextWindowTokens = modelContextWindow ?? 0;
|
|
5398
|
-
const contextPercent =
|
|
5803
|
+
const contextWindowTokens = Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 ? modelContextWindow : 0;
|
|
5804
|
+
const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
|
|
5399
5805
|
const toolPercent = computeToolCharPercentage(branch);
|
|
5400
5806
|
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
5401
5807
|
const estimator = makeTokenEstimator(summaryModel.provider, summaryModel.id, tokenCalibration);
|
|
@@ -5461,10 +5867,11 @@ function planManualPreflight(ctx, summaryModel, mode, tokenCalibration, config,
|
|
|
5461
5867
|
}
|
|
5462
5868
|
|
|
5463
5869
|
// src/ui/overlays.ts
|
|
5464
|
-
import
|
|
5870
|
+
import path10 from "path";
|
|
5465
5871
|
|
|
5466
5872
|
// src/utils/state.ts
|
|
5467
5873
|
import fs6 from "fs";
|
|
5874
|
+
import path9 from "path";
|
|
5468
5875
|
function getStatePath(projectId, state) {
|
|
5469
5876
|
if (!state?.scope)
|
|
5470
5877
|
return compactionStateFile(projectId);
|
|
@@ -5510,9 +5917,30 @@ function freshState(fp, data) {
|
|
|
5510
5917
|
}
|
|
5511
5918
|
return sanitizeCompactionStateEvidence(data);
|
|
5512
5919
|
}
|
|
5920
|
+
function pruneScopedStateSnapshots(target) {
|
|
5921
|
+
try {
|
|
5922
|
+
const dir = path9.dirname(target);
|
|
5923
|
+
const snapshots = fs6.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => {
|
|
5924
|
+
const file = path9.join(dir, entry.name);
|
|
5925
|
+
return { file, mtimeMs: fs6.statSync(file).mtimeMs };
|
|
5926
|
+
}).filter((entry) => entry.file !== target).sort((a, b) => b.mtimeMs - a.mtimeMs || b.file.localeCompare(a.file));
|
|
5927
|
+
for (const snapshot of snapshots.slice(Math.max(0, STATE_SNAPSHOT_MAX_FILES - 1))) {
|
|
5928
|
+
try {
|
|
5929
|
+
fs6.unlinkSync(snapshot.file);
|
|
5930
|
+
} catch (error2) {
|
|
5931
|
+
debug("state snapshot cleanup failed", error2);
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
} catch (error2) {
|
|
5935
|
+
debug("state snapshot retention failed", error2);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5513
5938
|
function saveCompactionState(projectId, state) {
|
|
5514
5939
|
try {
|
|
5515
|
-
|
|
5940
|
+
const target = getStatePath(projectId, state);
|
|
5941
|
+
writeJsonSync(target, sanitizeCompactionStateEvidence(state), true);
|
|
5942
|
+
if (state.scope)
|
|
5943
|
+
pruneScopedStateSnapshots(target);
|
|
5516
5944
|
return true;
|
|
5517
5945
|
} catch (error2) {
|
|
5518
5946
|
warn("saveCompactionState failed", error2);
|
|
@@ -5520,6 +5948,11 @@ function saveCompactionState(projectId, state) {
|
|
|
5520
5948
|
}
|
|
5521
5949
|
}
|
|
5522
5950
|
function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
5951
|
+
const snapshotProbe = scopedCompactionStateFile(scope.projectId, scope.sessionId, "__snapshot__");
|
|
5952
|
+
let availableSnapshots = new Set;
|
|
5953
|
+
try {
|
|
5954
|
+
availableSnapshots = new Set(fs6.readdirSync(path9.dirname(snapshotProbe), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name));
|
|
5955
|
+
} catch {}
|
|
5523
5956
|
const ancestry = Array.from(new Set([
|
|
5524
5957
|
...branchEntryIds2,
|
|
5525
5958
|
...scope.branchHeadId ? [scope.branchHeadId] : []
|
|
@@ -5527,6 +5960,8 @@ function loadScopedCompactionState(scope, branchEntryIds2 = []) {
|
|
|
5527
5960
|
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
5961
|
for (const branchHeadId of ancestry) {
|
|
5529
5962
|
const fp = scopedCompactionStateFile(scope.projectId, scope.sessionId, branchHeadId);
|
|
5963
|
+
if (!availableSnapshots.has(path9.basename(fp)))
|
|
5964
|
+
continue;
|
|
5530
5965
|
const state = freshState(fp, readJsonSync(fp));
|
|
5531
5966
|
if (valid(state, branchHeadId))
|
|
5532
5967
|
return state;
|
|
@@ -5589,6 +6024,7 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
5589
6024
|
const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
|
|
5590
6025
|
return {
|
|
5591
6026
|
goal: extraction.mainGoal,
|
|
6027
|
+
goalKey: extraction.mainGoal ? normalizeFactKey(extraction.mainGoal) : undefined,
|
|
5592
6028
|
decisions: extraction.decisions.map((d) => ({
|
|
5593
6029
|
id: ID_PREFIX.DECISION + ++decisionId,
|
|
5594
6030
|
summary: d.summary.slice(0, TRUNC.DECISION_SUMMARY),
|
|
@@ -5662,10 +6098,13 @@ function mergeCompactionStates(previous, current) {
|
|
|
5662
6098
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
5663
6099
|
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
6100
|
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops);
|
|
5665
|
-
const
|
|
6101
|
+
const currentGoalKey = activeCurrent.goalKey ?? (activeCurrent.goal ? normalizeFactKey(activeCurrent.goal) : "");
|
|
6102
|
+
const previousGoalKey = activePrevious.goalKey ?? (activePrevious.goal ? normalizeFactKey(activePrevious.goal) : "");
|
|
6103
|
+
const oldGoal = previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey ? ["Previous goal: " + activePrevious.goal] : [];
|
|
5666
6104
|
return applyContinuityOverrides({
|
|
5667
6105
|
...activeCurrent,
|
|
5668
6106
|
goal: activeCurrent.goal ?? activePrevious.goal,
|
|
6107
|
+
goalKey: activeCurrent.goal ? currentGoalKey || undefined : (activePrevious.goalKey ?? previousGoalKey) || undefined,
|
|
5669
6108
|
decisions,
|
|
5670
6109
|
constraints,
|
|
5671
6110
|
modifiedFiles: mergeBy(activeCurrent.modifiedFiles, activePrevious.modifiedFiles.filter((file) => !currentDeleted.has(normalizeFactKey(file))), normalizeFactKey, 100),
|
|
@@ -5757,7 +6196,9 @@ function computeDelta(prev, current) {
|
|
|
5757
6196
|
]);
|
|
5758
6197
|
const resolvedErrors = prev.unresolvedErrors.filter((e) => resolvedErrorKeys.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5759
6198
|
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(normalizeFactKey(e.message))).map((e) => e.message);
|
|
5760
|
-
const
|
|
6199
|
+
const previousGoalKey = prev.goalKey ?? (prev.goal ? normalizeFactKey(prev.goal) : "");
|
|
6200
|
+
const currentGoalKey = current.goalKey ?? (current.goal ? normalizeFactKey(current.goal) : "");
|
|
6201
|
+
const goalChanged = Boolean(previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey);
|
|
5761
6202
|
return {
|
|
5762
6203
|
newDecisions,
|
|
5763
6204
|
removedDecisions,
|
|
@@ -5824,7 +6265,7 @@ function ensurePinnedPaths(summary, pinned) {
|
|
|
5824
6265
|
if (!pinned.length)
|
|
5825
6266
|
return summary;
|
|
5826
6267
|
const lower = summary.toLowerCase();
|
|
5827
|
-
const missing = pinned.map((
|
|
6268
|
+
const missing = pinned.map((path10) => summaryEvidenceLine(path10, TRUNC.MESSAGE)).filter((path10) => path10 && !lower.includes(path10.toLowerCase()));
|
|
5828
6269
|
if (!missing.length)
|
|
5829
6270
|
return summary;
|
|
5830
6271
|
const parsed = parseSummary(summary);
|
|
@@ -6123,7 +6564,7 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
6123
6564
|
const f = modFiles[i];
|
|
6124
6565
|
const fc = extraction.modifiedFiles.find((e) => e.path === f);
|
|
6125
6566
|
const count = fc ? " (" + fc.toolCalls + "x)" : "";
|
|
6126
|
-
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text",
|
|
6567
|
+
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path10.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
|
|
6127
6568
|
}
|
|
6128
6569
|
if (modFiles.length > maxShow) {
|
|
6129
6570
|
c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
|
|
@@ -6660,10 +7101,13 @@ function asBranchMessage(message) {
|
|
|
6660
7101
|
function asSerializableMessages(msgs) {
|
|
6661
7102
|
return msgs;
|
|
6662
7103
|
}
|
|
7104
|
+
function scrubLlmMessages(msgs, scrubber) {
|
|
7105
|
+
return scrubber.scrubValue(msgs).value;
|
|
7106
|
+
}
|
|
6663
7107
|
|
|
6664
7108
|
// src/utils/session-log.ts
|
|
6665
7109
|
import * as fs7 from "fs";
|
|
6666
|
-
import * as
|
|
7110
|
+
import * as path11 from "path";
|
|
6667
7111
|
import { StringDecoder } from "string_decoder";
|
|
6668
7112
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
6669
7113
|
function getSessionsDir() {
|
|
@@ -6706,35 +7150,54 @@ function getMaxEntries() {
|
|
|
6706
7150
|
}
|
|
6707
7151
|
var logPathCache = new Map;
|
|
6708
7152
|
var messageMapCache = new Map;
|
|
6709
|
-
function
|
|
7153
|
+
function sessionDirectoryForCwd(cwd) {
|
|
7154
|
+
const safeCwd = path11.resolve(cwd).replace(/^[/\\]/, "").replace(/[:/\\]/g, "-");
|
|
7155
|
+
return path11.join(getSessionsDir(), "--" + safeCwd + "--");
|
|
7156
|
+
}
|
|
7157
|
+
function findLogInDirectory(directory, sessionId) {
|
|
7158
|
+
if (!fs7.existsSync(directory))
|
|
7159
|
+
return null;
|
|
7160
|
+
if (/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
|
|
7161
|
+
const exact = path11.join(directory, sessionId + ".jsonl");
|
|
7162
|
+
if (fs7.existsSync(exact))
|
|
7163
|
+
return exact;
|
|
7164
|
+
}
|
|
7165
|
+
const match = fs7.readdirSync(directory, { withFileTypes: true }).find((entry) => entry.isFile() && entry.name.endsWith("_" + sessionId + ".jsonl"));
|
|
7166
|
+
return match ? path11.join(directory, match.name) : null;
|
|
7167
|
+
}
|
|
7168
|
+
function findSessionLogFile(sessionId, cwd) {
|
|
6710
7169
|
const home2 = process.env.HOME ?? "/tmp";
|
|
6711
7170
|
const now = Date.now();
|
|
6712
|
-
const
|
|
6713
|
-
|
|
6714
|
-
|
|
7171
|
+
const directDirectory = cwd ? sessionDirectoryForCwd(cwd) : null;
|
|
7172
|
+
const cacheKey = sessionId + "\x00" + (directDirectory ?? "*");
|
|
7173
|
+
const remember = (foundPath) => {
|
|
7174
|
+
lruSet(logPathCache, cacheKey, { path: foundPath, expiresAt: now + LOG_PATH_CACHE_TTL_MS, home: home2 }, getMaxEntries());
|
|
7175
|
+
return foundPath;
|
|
6715
7176
|
};
|
|
6716
7177
|
try {
|
|
6717
|
-
const cached = lruGet(logPathCache,
|
|
7178
|
+
const cached = lruGet(logPathCache, cacheKey);
|
|
6718
7179
|
if (cached && cached.home === home2 && cached.expiresAt > now)
|
|
6719
7180
|
return cached.path;
|
|
6720
7181
|
const sessionsDir2 = getSessionsDir();
|
|
6721
7182
|
if (!fs7.existsSync(sessionsDir2))
|
|
6722
7183
|
return remember(null);
|
|
6723
|
-
|
|
6724
|
-
const
|
|
6725
|
-
|
|
6726
|
-
|
|
7184
|
+
if (directDirectory) {
|
|
7185
|
+
const direct = findLogInDirectory(directDirectory, sessionId);
|
|
7186
|
+
if (direct)
|
|
7187
|
+
return remember(direct);
|
|
7188
|
+
}
|
|
7189
|
+
for (const subdir of fs7.readdirSync(sessionsDir2, { withFileTypes: true })) {
|
|
7190
|
+
if (!subdir.isDirectory())
|
|
7191
|
+
continue;
|
|
7192
|
+
const subdirPath = path11.join(sessionsDir2, subdir.name);
|
|
7193
|
+
if (subdirPath === directDirectory)
|
|
6727
7194
|
continue;
|
|
6728
|
-
const
|
|
6729
|
-
if (
|
|
6730
|
-
return remember(
|
|
6731
|
-
const files = fs7.readdirSync(subdirPath);
|
|
6732
|
-
const match = files.find((f) => f.endsWith("_" + sessionId + ".jsonl"));
|
|
6733
|
-
if (match)
|
|
6734
|
-
return remember(path10.join(subdirPath, match));
|
|
7195
|
+
const found = findLogInDirectory(subdirPath, sessionId);
|
|
7196
|
+
if (found)
|
|
7197
|
+
return remember(found);
|
|
6735
7198
|
}
|
|
6736
|
-
} catch (
|
|
6737
|
-
debug("findSessionLogFile failed",
|
|
7199
|
+
} catch (error2) {
|
|
7200
|
+
debug("findSessionLogFile failed", error2);
|
|
6738
7201
|
}
|
|
6739
7202
|
return remember(null);
|
|
6740
7203
|
}
|
|
@@ -6758,8 +7221,8 @@ function normalizeLogMessage(msg, entryTimestamp) {
|
|
|
6758
7221
|
function hasTruncatedMessages(msgs) {
|
|
6759
7222
|
return msgs.some((m) => TRUNCATE_RE.test(extractText(m.content)));
|
|
6760
7223
|
}
|
|
6761
|
-
async function readOriginalMessageMap(sessionId, wantedIds) {
|
|
6762
|
-
const logPath = findSessionLogFile(sessionId);
|
|
7224
|
+
async function readOriginalMessageMap(sessionId, wantedIds, cwd) {
|
|
7225
|
+
const logPath = findSessionLogFile(sessionId, cwd);
|
|
6763
7226
|
if (!logPath) {
|
|
6764
7227
|
debug("Session log not found for " + sessionId);
|
|
6765
7228
|
return null;
|
|
@@ -6804,9 +7267,9 @@ async function readOriginalMessageMap(sessionId, wantedIds) {
|
|
|
6804
7267
|
return null;
|
|
6805
7268
|
}
|
|
6806
7269
|
}
|
|
6807
|
-
async function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
7270
|
+
async function resolveCompactionMessages(sessionId, toCompactEntries, cwd) {
|
|
6808
7271
|
const wantedIds = new Set(toCompactEntries.flatMap((entry) => entry.id ? [entry.id] : []));
|
|
6809
|
-
const logMap = await readOriginalMessageMap(sessionId, wantedIds);
|
|
7272
|
+
const logMap = await readOriginalMessageMap(sessionId, wantedIds, cwd);
|
|
6810
7273
|
if (!logMap)
|
|
6811
7274
|
return null;
|
|
6812
7275
|
let restoredCount = 0;
|
|
@@ -6840,7 +7303,7 @@ async function recoverSessionLog(rc) {
|
|
|
6840
7303
|
return convertToLlm2([asBranchMessage(entry.message)]).map((message) => ({ entryId: entry.id, message }));
|
|
6841
7304
|
});
|
|
6842
7305
|
if (hasTruncatedMessages(resolved.map((item) => item.message))) {
|
|
6843
|
-
const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact);
|
|
7306
|
+
const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact, rc.ctx.cwd);
|
|
6844
7307
|
if (fromLog) {
|
|
6845
7308
|
resolved = fromLog;
|
|
6846
7309
|
rc.notify("Using untruncated session log (" + resolved.length + " msgs)", "info");
|
|
@@ -7028,21 +7491,26 @@ function extractWithCache(rc) {
|
|
|
7028
7491
|
const currentEntryIds = rc.toCompact.map((e) => e.id);
|
|
7029
7492
|
const selectedMessages = rc.llmMessages;
|
|
7030
7493
|
const pruning = pruneRedundant(selectedMessages);
|
|
7494
|
+
const pruningUnchanged = pruning.messages.length === selectedMessages.length && pruning.messages.every((message, index) => message === selectedMessages[index]);
|
|
7031
7495
|
const currentKeptEntryIds = pruning.keptIndices.map((i) => rc.llmEntryIds[i]).filter((id) => typeof id === "string");
|
|
7032
7496
|
if (pruning.prunedCount > 0) {
|
|
7033
7497
|
rc.notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
7034
7498
|
}
|
|
7035
|
-
|
|
7499
|
+
const scrubbedMessages = scrubLlmMessages(pruning.messages, rc.services.scrubber);
|
|
7500
|
+
pruning.messages = scrubbedMessages;
|
|
7501
|
+
rc.llmMessages = scrubbedMessages;
|
|
7036
7502
|
const pruneEnd = Date.now();
|
|
7037
7503
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
7038
7504
|
const extractionStart = pruneEnd;
|
|
7039
|
-
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
7505
|
+
const convText = rc.services.scrubber.scrubText(serializeConversation(asSerializableMessages(rc.llmMessages))).value;
|
|
7040
7506
|
const convTokens = rc.estimator.text(convText);
|
|
7041
7507
|
let preparedBackup;
|
|
7042
7508
|
if (rc.config.backupEnabled) {
|
|
7043
|
-
const unchanged = pruning.messages.length === selectedMessages.length && pruning.messages.every((message, index) => message === selectedMessages[index]);
|
|
7044
7509
|
const materializeBackup = () => {
|
|
7045
|
-
|
|
7510
|
+
if (pruningUnchanged)
|
|
7511
|
+
return convText;
|
|
7512
|
+
const safeMessages = scrubLlmMessages(selectedMessages, rc.services.scrubber);
|
|
7513
|
+
const backupText = serializeConversation(asSerializableMessages(safeMessages));
|
|
7046
7514
|
return rc.services.scrubber.scrubText(backupText).value;
|
|
7047
7515
|
};
|
|
7048
7516
|
preparedBackup = prepareConversationBackup(materializeBackup, rc.sessionId, {
|
|
@@ -7058,6 +7526,7 @@ function extractWithCache(rc) {
|
|
|
7058
7526
|
const currentFirstId = rc.toCompact[0]?.id;
|
|
7059
7527
|
const currentLastId = rc.toCompact[rc.toCompact.length - 1]?.id;
|
|
7060
7528
|
let cacheUsable = false;
|
|
7529
|
+
let cacheExact = false;
|
|
7061
7530
|
let keptCount = 0;
|
|
7062
7531
|
if (cachedExt) {
|
|
7063
7532
|
const hasNewFp = !!(cachedExt.keptEntryIdsFp && cachedExt.entryIdsFp);
|
|
@@ -7066,9 +7535,11 @@ function extractWithCache(rc) {
|
|
|
7066
7535
|
const prunedPrefixMatch = hasNewFp ? isPrefixOf(cachedExt.keptEntryIdsFp, currentKeptEntryIds) : legacyPrefixMatch(cachedExt.keptEntryIds, currentKeptEntryIds);
|
|
7067
7536
|
keptCount = hasNewFp ? cachedExt.keptEntryIdsFp?.count ?? 0 : cachedExt.keptEntryIds?.length ?? 0;
|
|
7068
7537
|
if (hasNewFp || hasLegacy) {
|
|
7069
|
-
|
|
7538
|
+
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;
|
|
7539
|
+
cacheUsable = branchPrefixMatch && prunedPrefixMatch && boundedCacheShape && cachedExt.messageCount === keptCount && cachedExt.messageCount <= rc.llmMessages.length;
|
|
7540
|
+
cacheExact = cacheUsable && cachedExt.messageCount === rc.llmMessages.length;
|
|
7070
7541
|
if (!cacheUsable) {
|
|
7071
|
-
missReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : cachedExt.messageCount !== keptCount ? "cache-shape-mismatch" : "
|
|
7542
|
+
missReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : !boundedCacheShape ? "cache-evidence-unbounded" : cachedExt.messageCount !== keptCount ? "cache-shape-mismatch" : "cache-domain-ahead";
|
|
7072
7543
|
}
|
|
7073
7544
|
} else {
|
|
7074
7545
|
missReason = "legacy-no-kept-entryids";
|
|
@@ -7076,12 +7547,18 @@ function extractWithCache(rc) {
|
|
|
7076
7547
|
}
|
|
7077
7548
|
}
|
|
7078
7549
|
if (cacheUsable && cachedExt) {
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7550
|
+
if (cacheExact) {
|
|
7551
|
+
extraction = cachedExt.extraction;
|
|
7552
|
+
rc.notify("Phase 1 Cached: exact pruned conversation reused", "info");
|
|
7553
|
+
rc.vlog("Exact extraction cache hit \u2014 " + cachedExt.messageCount + " pruned messages");
|
|
7554
|
+
} else {
|
|
7555
|
+
const newMsgs = rc.llmMessages.slice(cachedExt.messageCount);
|
|
7556
|
+
const deltaTcIdx = buildToolCallIndex(newMsgs);
|
|
7557
|
+
const delta = extractStructured(newMsgs, rc.profileCfg, deltaTcIdx);
|
|
7558
|
+
extraction = mergeExtractions(cachedExt.extraction, delta, cachedExt.messageCount, newMsgs, deltaTcIdx);
|
|
7559
|
+
rc.notify("Phase 1 Incremental: " + cachedExt.messageCount + " cached + " + newMsgs.length + " new pruned messages", "info");
|
|
7560
|
+
rc.vlog("Incremental extraction \u2014 cached pruned messages: " + cachedExt.messageCount + ", current pruned: " + rc.llmMessages.length);
|
|
7561
|
+
}
|
|
7085
7562
|
missReason = undefined;
|
|
7086
7563
|
recordExtractionCacheHit(rc.services);
|
|
7087
7564
|
} else {
|
|
@@ -7189,25 +7666,61 @@ var EXPLORATION_TOOLS = [
|
|
|
7189
7666
|
parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
|
|
7190
7667
|
}
|
|
7191
7668
|
];
|
|
7192
|
-
function
|
|
7669
|
+
function boundedExplorationValue(value, depth = 0) {
|
|
7670
|
+
if (typeof value === "string") {
|
|
7671
|
+
return value.length > TRUNC.PREVIEW_XL ? value.slice(0, TRUNC.PREVIEW_XL) + "\u2026" : value;
|
|
7672
|
+
}
|
|
7673
|
+
if (value == null || typeof value !== "object")
|
|
7674
|
+
return value;
|
|
7675
|
+
if (depth >= 3)
|
|
7676
|
+
return "[bounded]";
|
|
7677
|
+
if (Array.isArray(value))
|
|
7678
|
+
return value.slice(0, 50).map((item) => boundedExplorationValue(item, depth + 1));
|
|
7679
|
+
return Object.fromEntries(Object.entries(value).slice(0, 16).map(([key, item]) => [key, boundedExplorationValue(item, depth + 1)]));
|
|
7680
|
+
}
|
|
7681
|
+
function serializeExplorationResult(value, scrubber) {
|
|
7682
|
+
const safe = boundedExplorationValue(scrubber.scrubValue(value).value);
|
|
7683
|
+
const serialized = JSON.stringify(safe);
|
|
7684
|
+
if (serialized.length <= MAX_EXPLORER_OUTPUT_CHARS)
|
|
7685
|
+
return serialized;
|
|
7686
|
+
let excerptChars = Math.max(0, Math.floor((MAX_EXPLORER_OUTPUT_CHARS - 160) / 2));
|
|
7687
|
+
for (;; ) {
|
|
7688
|
+
const result = JSON.stringify({
|
|
7689
|
+
truncated: true,
|
|
7690
|
+
originalChars: serialized.length,
|
|
7691
|
+
head: serialized.slice(0, excerptChars),
|
|
7692
|
+
tail: serialized.slice(-excerptChars)
|
|
7693
|
+
});
|
|
7694
|
+
if (result.length <= MAX_EXPLORER_OUTPUT_CHARS)
|
|
7695
|
+
return result;
|
|
7696
|
+
if (excerptChars === 0)
|
|
7697
|
+
return JSON.stringify({ truncated: true, originalChars: serialized.length });
|
|
7698
|
+
excerptChars = Math.max(0, excerptChars - Math.max(1, result.length - MAX_EXPLORER_OUTPUT_CHARS));
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
function executeExplorationTool(call, llmMessages, scrubber = new SecretScrubber) {
|
|
7193
7702
|
const args = call.arguments ?? {};
|
|
7194
7703
|
const boundedInteger = (value, fallback, min, max) => typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, Math.trunc(value))) : fallback;
|
|
7704
|
+
let output;
|
|
7195
7705
|
switch (call.name) {
|
|
7196
7706
|
case "get_message_range": {
|
|
7197
7707
|
const s = boundedInteger(args.start, 0, 0, llmMessages.length);
|
|
7198
7708
|
const e = boundedInteger(args.end, llmMessages.length, s, llmMessages.length);
|
|
7199
|
-
|
|
7709
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7200
7710
|
idx: s + i,
|
|
7201
7711
|
role: m?.role,
|
|
7202
7712
|
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW),
|
|
7203
7713
|
toolCalls: getToolCallNames(m?.content),
|
|
7204
7714
|
isError: m?.isError
|
|
7205
|
-
}))
|
|
7715
|
+
}));
|
|
7716
|
+
break;
|
|
7206
7717
|
}
|
|
7207
7718
|
case "search_conversation": {
|
|
7208
|
-
const q =
|
|
7209
|
-
if (!q
|
|
7210
|
-
|
|
7719
|
+
const q = typeof args.query === "string" ? args.query.toLowerCase().trim() : "";
|
|
7720
|
+
if (!q) {
|
|
7721
|
+
output = [{ error: "query must be a non-empty string" }];
|
|
7722
|
+
break;
|
|
7723
|
+
}
|
|
7211
7724
|
const matches = [];
|
|
7212
7725
|
for (let i = 0;i < llmMessages.length && matches.length < 10; i++) {
|
|
7213
7726
|
const m = llmMessages[i];
|
|
@@ -7216,68 +7729,91 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
7216
7729
|
matches.push({ idx: i, m });
|
|
7217
7730
|
continue;
|
|
7218
7731
|
}
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7732
|
+
let argumentsMatch = false;
|
|
7733
|
+
for (const tc of filterToolCalls(m?.content)) {
|
|
7734
|
+
const stack = [{ value: tc.arguments, depth: 0 }];
|
|
7735
|
+
let inspected = 0;
|
|
7736
|
+
while (stack.length && inspected++ < 64 && !argumentsMatch) {
|
|
7737
|
+
const current = stack.pop();
|
|
7738
|
+
if (typeof current.value === "string") {
|
|
7739
|
+
argumentsMatch = current.value.slice(0, 2000).toLowerCase().includes(q);
|
|
7740
|
+
} else if (current.value && typeof current.value === "object" && current.depth < 3) {
|
|
7741
|
+
const values = Array.isArray(current.value) ? current.value.slice(0, 16) : Object.values(current.value).slice(0, 16);
|
|
7742
|
+
for (const value of values)
|
|
7743
|
+
stack.push({ value, depth: current.depth + 1 });
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
if (argumentsMatch)
|
|
7747
|
+
break;
|
|
7222
7748
|
}
|
|
7749
|
+
if (argumentsMatch)
|
|
7750
|
+
matches.push({ idx: i, m });
|
|
7223
7751
|
}
|
|
7224
|
-
|
|
7752
|
+
output = matches.map(({ idx, m }) => ({
|
|
7225
7753
|
idx,
|
|
7226
7754
|
role: m?.role,
|
|
7227
7755
|
preview: extractText(m?.content).slice(0, TRUNC.PREVIEW)
|
|
7228
|
-
}))
|
|
7756
|
+
}));
|
|
7757
|
+
break;
|
|
7229
7758
|
}
|
|
7230
7759
|
case "get_recent_user_messages": {
|
|
7231
7760
|
const count = boundedInteger(args.count, 10, 1, 50);
|
|
7232
|
-
|
|
7761
|
+
output = llmMessages.filter((m) => m?.role === "user").slice(-count).map((m) => extractText(m.content).slice(0, TRUNC.PREVIEW_XL));
|
|
7762
|
+
break;
|
|
7233
7763
|
}
|
|
7234
7764
|
case "get_context_around": {
|
|
7235
7765
|
const idx = boundedInteger(args.index, 0, 0, Math.max(0, llmMessages.length - 1));
|
|
7236
7766
|
const radius = boundedInteger(args.radius, 5, 0, 25);
|
|
7237
7767
|
const s = Math.max(0, idx - radius), e = Math.min(llmMessages.length, idx + radius + 1);
|
|
7238
|
-
|
|
7768
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7239
7769
|
idx: s + i,
|
|
7240
7770
|
role: m?.role,
|
|
7241
7771
|
text: extractText(m?.content).slice(0, TRUNC.DETAIL),
|
|
7242
7772
|
toolCalls: getToolCallNames(m?.content),
|
|
7243
7773
|
isError: m?.isError
|
|
7244
|
-
}))
|
|
7774
|
+
}));
|
|
7775
|
+
break;
|
|
7245
7776
|
}
|
|
7246
7777
|
case "get_file_changes": {
|
|
7247
|
-
const target =
|
|
7248
|
-
if (!target
|
|
7249
|
-
|
|
7778
|
+
const target = typeof args.path === "string" ? args.path.toLowerCase().trim() : "";
|
|
7779
|
+
if (!target) {
|
|
7780
|
+
output = [{ error: "path must be a non-empty string" }];
|
|
7781
|
+
break;
|
|
7782
|
+
}
|
|
7250
7783
|
const results = [];
|
|
7251
|
-
for (let i = 0;i < llmMessages.length; i++) {
|
|
7252
|
-
const
|
|
7253
|
-
for (const block of tcs) {
|
|
7784
|
+
for (let i = 0;i < llmMessages.length && results.length < TRUNC.EXPLORE_RESULTS; i++) {
|
|
7785
|
+
for (const block of filterToolCalls(llmMessages[i]?.content)) {
|
|
7254
7786
|
const a = block.arguments ?? {};
|
|
7255
|
-
const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7787
|
+
const fileFields = [a.path, a.file, a.filePath, a.file_path].filter((value) => typeof value === "string").map((value) => value.toLowerCase());
|
|
7788
|
+
if (classifyTool(block.arguments) !== "mutates" || !fileFields.some((file) => file.includes(target)))
|
|
7789
|
+
continue;
|
|
7790
|
+
const preview = extractText(llmMessages[i]?.content).slice(0, TRUNC.PREVIEW_LONG);
|
|
7791
|
+
const surgicalKeys = ["path", "file", "filePath", "file_path", "oldText", "newText", "edits", "patch"];
|
|
7792
|
+
const argsPreview = Object.fromEntries(surgicalKeys.filter((key) => a[key] !== undefined).map((key) => [key, a[key]]));
|
|
7793
|
+
const surgical = a.oldText != null || a.newText != null || a.edits != null || a.patch != null;
|
|
7794
|
+
results.push(surgical ? { idx: i, role: "assistant", toolCall: block.name ?? "mutates", args: argsPreview, preview } : { idx: i, role: "assistant", toolCall: block.name ?? "mutates", preview });
|
|
7262
7795
|
}
|
|
7263
7796
|
}
|
|
7264
|
-
|
|
7797
|
+
output = results.length ? results : [{ info: "No edits found for: " + args.path }];
|
|
7798
|
+
break;
|
|
7265
7799
|
}
|
|
7266
7800
|
case "get_error_chain": {
|
|
7267
7801
|
const errIdx = boundedInteger(args.index, 0, 0, Math.max(0, llmMessages.length - 1));
|
|
7268
7802
|
const ctxRadius = boundedInteger(args.context_radius, 8, 0, 25);
|
|
7269
7803
|
const s = Math.max(0, errIdx - ctxRadius), e = Math.min(llmMessages.length, errIdx + ctxRadius + 1);
|
|
7270
|
-
|
|
7804
|
+
output = llmMessages.slice(s, e).map((m, i) => ({
|
|
7271
7805
|
idx: s + i,
|
|
7272
7806
|
role: m?.role,
|
|
7273
7807
|
text: extractText(m?.content).slice(0, TRUNC.PREVIEW_XL),
|
|
7274
7808
|
isError: m?.isError,
|
|
7275
7809
|
toolCalls: getToolCallNames(m?.content)
|
|
7276
|
-
}))
|
|
7810
|
+
}));
|
|
7811
|
+
break;
|
|
7277
7812
|
}
|
|
7278
7813
|
default:
|
|
7279
|
-
|
|
7814
|
+
output = { error: "Unknown tool: " + call.name };
|
|
7280
7815
|
}
|
|
7816
|
+
return serializeExplorationResult(output, scrubber);
|
|
7281
7817
|
}
|
|
7282
7818
|
function parseExplorationReport(text, llmMessages) {
|
|
7283
7819
|
let json = text.trim();
|
|
@@ -7424,7 +7960,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
7424
7960
|
probeResp
|
|
7425
7961
|
];
|
|
7426
7962
|
for (const tc of toolCalls) {
|
|
7427
|
-
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
7963
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages, svc.scrubber);
|
|
7428
7964
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
7429
7965
|
}
|
|
7430
7966
|
let rounds = 1;
|
|
@@ -7457,7 +7993,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
7457
7993
|
}
|
|
7458
7994
|
messages.push(response);
|
|
7459
7995
|
for (const tc of nextToolCalls) {
|
|
7460
|
-
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
7996
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages, svc.scrubber);
|
|
7461
7997
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
7462
7998
|
}
|
|
7463
7999
|
}
|
|
@@ -7737,7 +8273,41 @@ function fitChunkBudget(messages, maxTokens, estimator) {
|
|
|
7737
8273
|
break;
|
|
7738
8274
|
estimate = estimateChunkTokens(fitted, estimator);
|
|
7739
8275
|
}
|
|
7740
|
-
|
|
8276
|
+
if (estimate <= maxTokens)
|
|
8277
|
+
return fitted;
|
|
8278
|
+
const rendered = fitted.map(renderBatchMessage).join(`
|
|
8279
|
+
`);
|
|
8280
|
+
const marker = "[\u2026chunk evidence hard-bounded for synthesis\u2026]";
|
|
8281
|
+
const candidate = (chars) => {
|
|
8282
|
+
if (chars <= 0)
|
|
8283
|
+
return [{ role: "user", content: marker }];
|
|
8284
|
+
const head = Math.ceil(chars * 0.6);
|
|
8285
|
+
return [{
|
|
8286
|
+
role: "user",
|
|
8287
|
+
content: rendered.slice(0, head) + `
|
|
8288
|
+
` + marker + `
|
|
8289
|
+
` + rendered.slice(-(chars - head))
|
|
8290
|
+
}];
|
|
8291
|
+
};
|
|
8292
|
+
let best = candidate(0);
|
|
8293
|
+
if (estimateChunkTokens(best, estimator) > maxTokens) {
|
|
8294
|
+
if (estimateChunkTokens([], estimator) <= maxTokens)
|
|
8295
|
+
return [];
|
|
8296
|
+
throw new RangeError("Token estimator cannot represent a chunk within maxChunkTokens");
|
|
8297
|
+
}
|
|
8298
|
+
let low = 0;
|
|
8299
|
+
let high = rendered.length;
|
|
8300
|
+
while (low <= high) {
|
|
8301
|
+
const middle = Math.floor((low + high) / 2);
|
|
8302
|
+
const next = candidate(middle);
|
|
8303
|
+
if (estimateChunkTokens(next, estimator) <= maxTokens) {
|
|
8304
|
+
best = next;
|
|
8305
|
+
low = middle + 1;
|
|
8306
|
+
} else {
|
|
8307
|
+
high = middle - 1;
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
return best;
|
|
7741
8311
|
}
|
|
7742
8312
|
function extendThroughToolResults(messages, start, proposedEnd) {
|
|
7743
8313
|
const callIndexes = new Map;
|
|
@@ -7765,7 +8335,10 @@ function splitOversizedChunk(ch, maxTokens, estimator) {
|
|
|
7765
8335
|
return [ch];
|
|
7766
8336
|
if (ch.messages.length <= 1) {
|
|
7767
8337
|
const messages = fitChunkBudget(ch.messages, maxTokens, estimator);
|
|
7768
|
-
|
|
8338
|
+
const tokenEstimate = estimateChunkTokens(messages, estimator);
|
|
8339
|
+
if (tokenEstimate > maxTokens)
|
|
8340
|
+
throw new RangeError("Chunk budget postcondition failed");
|
|
8341
|
+
return [{ ...ch, tokenEstimate, messages }];
|
|
7769
8342
|
}
|
|
7770
8343
|
const parts = [];
|
|
7771
8344
|
let start = 0;
|
|
@@ -7781,11 +8354,14 @@ function splitOversizedChunk(ch, maxTokens, estimator) {
|
|
|
7781
8354
|
}
|
|
7782
8355
|
const end = extendThroughToolResults(ch.messages, start, proposedEnd);
|
|
7783
8356
|
const messages = fitChunkBudget(ch.messages.slice(start, end), maxTokens, estimator);
|
|
8357
|
+
const tokenEstimate = estimateChunkTokens(messages, estimator);
|
|
8358
|
+
if (tokenEstimate > maxTokens)
|
|
8359
|
+
throw new RangeError("Chunk budget postcondition failed");
|
|
7784
8360
|
parts.push({
|
|
7785
8361
|
...ch,
|
|
7786
8362
|
startIndex: ch.startIndex + start,
|
|
7787
8363
|
endIndex: ch.startIndex + end - 1,
|
|
7788
|
-
tokenEstimate
|
|
8364
|
+
tokenEstimate,
|
|
7789
8365
|
messages
|
|
7790
8366
|
});
|
|
7791
8367
|
start = end;
|
|
@@ -8361,24 +8937,24 @@ async function summarizeConversation(rc) {
|
|
|
8361
8937
|
generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
|
|
8362
8938
|
}
|
|
8363
8939
|
let completed = totalBatches - batchCallLimit;
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8940
|
+
let nextBatch = 0;
|
|
8941
|
+
let budgetStopped = false;
|
|
8942
|
+
const runWorker = async () => {
|
|
8943
|
+
while (true) {
|
|
8944
|
+
const idx = nextBatch++;
|
|
8945
|
+
if (idx >= batchCallLimit)
|
|
8946
|
+
return;
|
|
8947
|
+
if (budgetStopped || rc.services.budget.reason()) {
|
|
8948
|
+
budgetStopped = true;
|
|
8949
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8950
|
+
} else {
|
|
8951
|
+
try {
|
|
8952
|
+
const batch = batches[idx];
|
|
8953
|
+
results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
|
|
8954
|
+
} catch (err) {
|
|
8955
|
+
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
8956
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8957
|
+
}
|
|
8382
8958
|
}
|
|
8383
8959
|
completed++;
|
|
8384
8960
|
showProgressOverlay(rc.ctx, {
|
|
@@ -8392,8 +8968,14 @@ async function summarizeConversation(rc) {
|
|
|
8392
8968
|
totalBatches,
|
|
8393
8969
|
currentBatch: completed
|
|
8394
8970
|
});
|
|
8395
|
-
}
|
|
8396
|
-
|
|
8971
|
+
}
|
|
8972
|
+
};
|
|
8973
|
+
const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
|
|
8974
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
8975
|
+
if (budgetStopped) {
|
|
8976
|
+
rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
|
|
8977
|
+
cacheable = false;
|
|
8978
|
+
generationFallbacks.push("synthesis budget exhausted during batch pool");
|
|
8397
8979
|
}
|
|
8398
8980
|
for (const r of results)
|
|
8399
8981
|
if (r)
|
|
@@ -8483,29 +9065,56 @@ function classifyOutcomeClaim(claim) {
|
|
|
8483
9065
|
return "file";
|
|
8484
9066
|
return "generic";
|
|
8485
9067
|
}
|
|
8486
|
-
|
|
9068
|
+
var successfulToolEvidenceCache = new WeakMap;
|
|
9069
|
+
function successfulToolEvidence(messages) {
|
|
9070
|
+
const cached = successfulToolEvidenceCache.get(messages);
|
|
9071
|
+
if (cached)
|
|
9072
|
+
return cached;
|
|
9073
|
+
const toolCalls = buildToolCallIndex(messages);
|
|
9074
|
+
const evidence = [];
|
|
9075
|
+
for (const message of messages) {
|
|
9076
|
+
if (message.role !== "toolResult" || message.isError)
|
|
9077
|
+
continue;
|
|
9078
|
+
const call = toolCalls.get(message.toolCallId ?? "");
|
|
9079
|
+
if (!call)
|
|
9080
|
+
continue;
|
|
9081
|
+
const result = extractText(message.content).slice(0, 8000);
|
|
9082
|
+
if (!result.trim() || LIKELY_ERROR_RE.test(result))
|
|
9083
|
+
continue;
|
|
9084
|
+
const command = [call.arguments.command, call.arguments.cmd, call.arguments.script].find((value) => typeof value === "string") ?? "";
|
|
9085
|
+
evidence.push({
|
|
9086
|
+
name: normalizeToolName(call.name),
|
|
9087
|
+
operation: classifyToolOperation(call.arguments, call.name),
|
|
9088
|
+
command,
|
|
9089
|
+
path: extractToolPath(call.arguments),
|
|
9090
|
+
result
|
|
9091
|
+
});
|
|
9092
|
+
}
|
|
9093
|
+
successfulToolEvidenceCache.set(messages, evidence);
|
|
9094
|
+
return evidence;
|
|
9095
|
+
}
|
|
9096
|
+
function successfulToolSupportsClaim(claim, tools, extraction) {
|
|
8487
9097
|
const shape = semanticShape(claim);
|
|
8488
9098
|
const category = classifyOutcomeClaim(claim);
|
|
8489
9099
|
if (category === "error" && extraction.errors.some((error2) => error2.resolved && hasSemanticEvidence(claim, error2.message)))
|
|
8490
9100
|
return true;
|
|
8491
9101
|
if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
|
|
8492
9102
|
return true;
|
|
8493
|
-
for (const
|
|
8494
|
-
|
|
9103
|
+
for (const tool of tools) {
|
|
9104
|
+
const operationText = tool.name + " " + tool.command;
|
|
9105
|
+
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";
|
|
9106
|
+
if (!operationSupports)
|
|
8495
9107
|
continue;
|
|
8496
|
-
|
|
8497
|
-
if (!bounded.trim() || LIKELY_ERROR_RE.test(bounded))
|
|
8498
|
-
continue;
|
|
8499
|
-
if (hasSemanticEvidence(claim, bounded))
|
|
9108
|
+
if (hasSemanticEvidence(claim, tool.result))
|
|
8500
9109
|
return true;
|
|
8501
|
-
const lower =
|
|
9110
|
+
const lower = tool.result.toLowerCase();
|
|
8502
9111
|
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
9112
|
return true;
|
|
8504
|
-
if (category === "build" && /\b(?:
|
|
9113
|
+
if (category === "build" && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
|
|
8505
9114
|
return true;
|
|
8506
|
-
if (category === "release" && /\b(?:
|
|
9115
|
+
if (category === "release" && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
|
|
8507
9116
|
return true;
|
|
8508
|
-
if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim,
|
|
9117
|
+
if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, tool.result))
|
|
8509
9118
|
return true;
|
|
8510
9119
|
}
|
|
8511
9120
|
return false;
|
|
@@ -8653,9 +9262,15 @@ function stemToken(token) {
|
|
|
8653
9262
|
function semanticTokens(text) {
|
|
8654
9263
|
return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
|
|
8655
9264
|
}
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
9265
|
+
var semanticShapeCache = new Map;
|
|
9266
|
+
var semanticFragmentCache = new Map;
|
|
9267
|
+
function semanticFragments(text) {
|
|
9268
|
+
const cached = lruGet(semanticFragmentCache, text);
|
|
9269
|
+
if (cached)
|
|
9270
|
+
return cached;
|
|
9271
|
+
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);
|
|
9272
|
+
lruSet(semanticFragmentCache, text, fragments, 256);
|
|
9273
|
+
return fragments;
|
|
8659
9274
|
}
|
|
8660
9275
|
function hasNearbyMarker(tokens, anchor, markers) {
|
|
8661
9276
|
return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
|
|
@@ -8679,20 +9294,24 @@ function hasEffectiveTargetNegation(tokens, anchor) {
|
|
|
8679
9294
|
});
|
|
8680
9295
|
}
|
|
8681
9296
|
function semanticShape(source) {
|
|
9297
|
+
const cached = lruGet(semanticShapeCache, source);
|
|
9298
|
+
if (cached)
|
|
9299
|
+
return cached;
|
|
8682
9300
|
const sourceTokens = semanticTokens(source);
|
|
8683
9301
|
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
9302
|
const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
|
|
8685
9303
|
const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
|
|
8686
9304
|
const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
|
|
8687
|
-
|
|
9305
|
+
const shape = { sourceTokens, concepts, anchor, negative, conditional };
|
|
9306
|
+
lruSet(semanticShapeCache, source, shape, 512);
|
|
9307
|
+
return shape;
|
|
8688
9308
|
}
|
|
8689
9309
|
function hasSemanticEvidence(source, target) {
|
|
8690
9310
|
const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
|
|
8691
9311
|
if (!concepts.length)
|
|
8692
9312
|
return true;
|
|
8693
9313
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
8694
|
-
return
|
|
8695
|
-
const tokens = semanticTokens(fragment);
|
|
9314
|
+
return semanticFragments(target).some((tokens) => {
|
|
8696
9315
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
8697
9316
|
if (overlap < required)
|
|
8698
9317
|
return false;
|
|
@@ -8714,8 +9333,7 @@ function hasSemanticContradiction(source, target) {
|
|
|
8714
9333
|
if (!anchor)
|
|
8715
9334
|
return false;
|
|
8716
9335
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
8717
|
-
return
|
|
8718
|
-
const tokens = semanticTokens(fragment);
|
|
9336
|
+
return semanticFragments(target).some((tokens) => {
|
|
8719
9337
|
if (!tokens.includes(anchor))
|
|
8720
9338
|
return false;
|
|
8721
9339
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
@@ -8906,16 +9524,18 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
8906
9524
|
gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
|
|
8907
9525
|
score -= 12;
|
|
8908
9526
|
}
|
|
9527
|
+
const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
|
|
8909
9528
|
for (const file of extraction.modifiedFiles) {
|
|
8910
|
-
const
|
|
8911
|
-
if (!
|
|
9529
|
+
const uniqueNeedles = buildUniquePathNeedles(file.path, modifiedPaths);
|
|
9530
|
+
if (!uniqueNeedles.some((needle) => doneRefs.has(normalizePath(needle))))
|
|
8912
9531
|
continue;
|
|
8913
9532
|
const unresolved = unresolvedEvidence.find((error2) => {
|
|
8914
9533
|
const firstLine = error2.message.split(/\r?\n/, 1)[0] ?? "";
|
|
8915
|
-
|
|
9534
|
+
const errorRefs = extractFileRefs(firstLine).map(normalizePath);
|
|
9535
|
+
return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
|
|
8916
9536
|
});
|
|
8917
9537
|
if (unresolved) {
|
|
8918
|
-
gaps.push({ kind: "inconsistency", detail:
|
|
9538
|
+
gaps.push({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
|
|
8919
9539
|
score -= 5;
|
|
8920
9540
|
}
|
|
8921
9541
|
}
|
|
@@ -8937,8 +9557,9 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
8937
9557
|
score -= 5;
|
|
8938
9558
|
}
|
|
8939
9559
|
if (evidence.sourceMessages) {
|
|
9560
|
+
const tools = successfulToolEvidence(evidence.sourceMessages);
|
|
8940
9561
|
for (const claim of outcomeClaims(summary)) {
|
|
8941
|
-
if (!successfulToolSupportsClaim(claim,
|
|
9562
|
+
if (!successfulToolSupportsClaim(claim, tools, extraction)) {
|
|
8942
9563
|
gaps.push({ kind: "unsupported-claim", claim });
|
|
8943
9564
|
score -= 20;
|
|
8944
9565
|
}
|
|
@@ -9050,6 +9671,28 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
|
|
|
9050
9671
|
}
|
|
9051
9672
|
return renderSummary(canonical, { canonicalHeadings: true });
|
|
9052
9673
|
}
|
|
9674
|
+
function hasUnclosedMarkdownFence(markdown) {
|
|
9675
|
+
let open = null;
|
|
9676
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
9677
|
+
const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
9678
|
+
if (!match)
|
|
9679
|
+
continue;
|
|
9680
|
+
const marker = match[1][0];
|
|
9681
|
+
if (!open) {
|
|
9682
|
+
open = { marker, length: match[1].length };
|
|
9683
|
+
} else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
|
|
9684
|
+
open = null;
|
|
9685
|
+
}
|
|
9686
|
+
}
|
|
9687
|
+
return open !== null;
|
|
9688
|
+
}
|
|
9689
|
+
function patchResponseIsTruncated(patched, stopReason) {
|
|
9690
|
+
const reason = String(stopReason ?? "");
|
|
9691
|
+
return /(?:length|truncat|max(?:imum)?[_ -]?(?:output[_ -]?)?tokens?|token[_ -]?limit)/i.test(reason) || /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
|
|
9692
|
+
}
|
|
9693
|
+
function sectionIdentity(section) {
|
|
9694
|
+
return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
|
|
9695
|
+
}
|
|
9053
9696
|
async function patchSummary(summary, gaps, model, auth, signal, services) {
|
|
9054
9697
|
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
9698
|
|
|
@@ -9069,7 +9712,13 @@ Return the COMPLETE corrected summary in the same format.`;
|
|
|
9069
9712
|
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
|
|
9070
9713
|
const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
|
|
9071
9714
|
`).trim();
|
|
9072
|
-
|
|
9715
|
+
if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
|
|
9716
|
+
return summary;
|
|
9717
|
+
const originalSections = parseSummary(summary).sections;
|
|
9718
|
+
const patchedSections = parseSummary(patched).sections;
|
|
9719
|
+
const patchedBodies = new Map(patchedSections.map((section) => [sectionIdentity(section), section.body.trim()]));
|
|
9720
|
+
const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
|
|
9721
|
+
return preserved ? patched : summary;
|
|
9073
9722
|
} catch (error2) {
|
|
9074
9723
|
debug("patchSummary LLM failed", error2);
|
|
9075
9724
|
return summary;
|
|
@@ -9186,7 +9835,7 @@ async function verifyAndPatch(rc) {
|
|
|
9186
9835
|
|
|
9187
9836
|
// src/app/steps/state.ts
|
|
9188
9837
|
import fs8 from "fs";
|
|
9189
|
-
import
|
|
9838
|
+
import path12 from "path";
|
|
9190
9839
|
|
|
9191
9840
|
// src/domain/yield-gate.ts
|
|
9192
9841
|
class YieldGateError extends Error {
|
|
@@ -9273,7 +9922,7 @@ function buildState(rc) {
|
|
|
9273
9922
|
currentState.factOverrides = prevState?.factOverrides ?? [];
|
|
9274
9923
|
let compactionState = mergeCompactionStates(prevState, currentState);
|
|
9275
9924
|
compactionState.deletedFiles = compactionState.deletedFiles.filter((file) => {
|
|
9276
|
-
const candidate =
|
|
9925
|
+
const candidate = path12.isAbsolute(file) ? file : path12.resolve(rc.ctx.cwd, file);
|
|
9277
9926
|
return !fs8.existsSync(candidate);
|
|
9278
9927
|
});
|
|
9279
9928
|
if (preserve.length > 0) {
|
|
@@ -9369,7 +10018,7 @@ function buildState(rc) {
|
|
|
9369
10018
|
// src/infra/context-graph.ts
|
|
9370
10019
|
import { createHash as createHash3 } from "crypto";
|
|
9371
10020
|
import fs9 from "fs";
|
|
9372
|
-
import
|
|
10021
|
+
import path13 from "path";
|
|
9373
10022
|
import { createRequire } from "module";
|
|
9374
10023
|
var require2 = createRequire(import.meta.url);
|
|
9375
10024
|
var MAX_PROJECT_NODES = 2000;
|
|
@@ -9377,7 +10026,27 @@ var MAX_MANUAL_NODES = 500;
|
|
|
9377
10026
|
var MAX_SESSION_NODES = 256;
|
|
9378
10027
|
var MAX_QUERY_CANDIDATES = 80;
|
|
9379
10028
|
var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
9380
|
-
var CONTEXT_GRAPH_SCHEMA_VERSION =
|
|
10029
|
+
var CONTEXT_GRAPH_SCHEMA_VERSION = 2;
|
|
10030
|
+
function bunSqliteAdapter(db) {
|
|
10031
|
+
return {
|
|
10032
|
+
exec: (sql) => db.exec(sql),
|
|
10033
|
+
query: (sql) => db.query(sql),
|
|
10034
|
+
transaction: (fn) => (...args) => {
|
|
10035
|
+
db.exec("BEGIN IMMEDIATE");
|
|
10036
|
+
try {
|
|
10037
|
+
const result = fn(...args);
|
|
10038
|
+
db.exec("COMMIT");
|
|
10039
|
+
return result;
|
|
10040
|
+
} catch (error2) {
|
|
10041
|
+
try {
|
|
10042
|
+
db.exec("ROLLBACK");
|
|
10043
|
+
} catch {}
|
|
10044
|
+
throw error2;
|
|
10045
|
+
}
|
|
10046
|
+
},
|
|
10047
|
+
close: () => db.close()
|
|
10048
|
+
};
|
|
10049
|
+
}
|
|
9381
10050
|
function nodeSqliteAdapter(db) {
|
|
9382
10051
|
return {
|
|
9383
10052
|
exec: (sql) => db.exec(sql),
|
|
@@ -9400,11 +10069,11 @@ function nodeSqliteAdapter(db) {
|
|
|
9400
10069
|
}
|
|
9401
10070
|
function openDatabase() {
|
|
9402
10071
|
const fp = contextGraphFile();
|
|
9403
|
-
ensureDir(
|
|
10072
|
+
ensureDir(path13.dirname(fp));
|
|
9404
10073
|
let db;
|
|
9405
10074
|
if ("bun" in process.versions) {
|
|
9406
10075
|
const { Database } = require2("bun:sqlite");
|
|
9407
|
-
db = new Database(fp);
|
|
10076
|
+
db = bunSqliteAdapter(new Database(fp));
|
|
9408
10077
|
} else {
|
|
9409
10078
|
const { DatabaseSync } = require2("node:sqlite");
|
|
9410
10079
|
db = nodeSqliteAdapter(new DatabaseSync(fp));
|
|
@@ -9432,6 +10101,8 @@ function openDatabase() {
|
|
|
9432
10101
|
);
|
|
9433
10102
|
CREATE INDEX IF NOT EXISTS context_nodes_project_status
|
|
9434
10103
|
ON context_nodes(project_id, status, updated_at DESC);
|
|
10104
|
+
CREATE INDEX IF NOT EXISTS context_nodes_lineage
|
|
10105
|
+
ON context_nodes(project_id, session_id, source, branch_head_id, kind, fact_key, updated_at DESC);
|
|
9435
10106
|
CREATE TABLE IF NOT EXISTS context_edges (
|
|
9436
10107
|
project_id TEXT NOT NULL,
|
|
9437
10108
|
from_id TEXT NOT NULL REFERENCES context_nodes(id) ON DELETE CASCADE,
|
|
@@ -9448,7 +10119,8 @@ function openDatabase() {
|
|
|
9448
10119
|
);
|
|
9449
10120
|
`);
|
|
9450
10121
|
const version = db.query("PRAGMA user_version").get();
|
|
9451
|
-
|
|
10122
|
+
const schemaVersion = Number(version?.user_version ?? 0);
|
|
10123
|
+
if (schemaVersion < 1) {
|
|
9452
10124
|
db.transaction(() => {
|
|
9453
10125
|
db.exec(`
|
|
9454
10126
|
DROP INDEX IF EXISTS context_nodes_fact;
|
|
@@ -9457,6 +10129,17 @@ function openDatabase() {
|
|
|
9457
10129
|
DELETE FROM context_nodes WHERE source = 'compaction';
|
|
9458
10130
|
CREATE UNIQUE INDEX context_nodes_fact
|
|
9459
10131
|
ON context_nodes(project_id, session_id, kind, fact_key, COALESCE(branch_head_id, ''));
|
|
10132
|
+
PRAGMA user_version = 1;
|
|
10133
|
+
`);
|
|
10134
|
+
})();
|
|
10135
|
+
}
|
|
10136
|
+
if (schemaVersion < 2) {
|
|
10137
|
+
db.transaction(() => {
|
|
10138
|
+
db.exec(`
|
|
10139
|
+
DELETE FROM context_nodes_fts;
|
|
10140
|
+
INSERT INTO context_nodes_fts(rowid, node_id, title, content, kind)
|
|
10141
|
+
SELECT rowid, id, title, content, kind FROM context_nodes
|
|
10142
|
+
WHERE status = 'active' AND kind NOT IN ('project', 'session');
|
|
9460
10143
|
PRAGMA user_version = ${CONTEXT_GRAPH_SCHEMA_VERSION};
|
|
9461
10144
|
`);
|
|
9462
10145
|
})();
|
|
@@ -9469,10 +10152,19 @@ function stableId(...parts) {
|
|
|
9469
10152
|
function factKey(text) {
|
|
9470
10153
|
return normalizeFactKey(text) || text.trim().toLowerCase();
|
|
9471
10154
|
}
|
|
10155
|
+
function removeFtsNode(db, nodeId) {
|
|
10156
|
+
db.query(`
|
|
10157
|
+
DELETE FROM context_nodes_fts
|
|
10158
|
+
WHERE rowid = (SELECT rowid FROM context_nodes WHERE id = ?)
|
|
10159
|
+
`).run(nodeId);
|
|
10160
|
+
}
|
|
9472
10161
|
function syncFts(db, node) {
|
|
9473
|
-
db.query("
|
|
10162
|
+
const row = db.query("SELECT rowid FROM context_nodes WHERE id = ?").get(node.id);
|
|
10163
|
+
if (!row)
|
|
10164
|
+
return;
|
|
10165
|
+
db.query("DELETE FROM context_nodes_fts WHERE rowid = ?").run(row.rowid);
|
|
9474
10166
|
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);
|
|
10167
|
+
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
10168
|
}
|
|
9477
10169
|
}
|
|
9478
10170
|
function upsertNode(db, node, refresh = false) {
|
|
@@ -9541,13 +10233,22 @@ function branchLineage(scope) {
|
|
|
9541
10233
|
return Array.from(new Set([...scope.branchEntryIds ?? [], scope.branchHeadId].filter((id) => typeof id === "string" && id.length > 0)));
|
|
9542
10234
|
}
|
|
9543
10235
|
function lineageFactRows(db, scope, kind, key) {
|
|
9544
|
-
const lineage =
|
|
9545
|
-
const
|
|
10236
|
+
const lineage = branchLineage(scope);
|
|
10237
|
+
const params = [scope.projectId, scope.sessionId];
|
|
10238
|
+
let branchClause = "AND branch_head_id IS NULL";
|
|
10239
|
+
if (lineage.length > 0) {
|
|
10240
|
+
branchClause = "AND branch_head_id IN (" + lineage.map(() => "?").join(",") + ")";
|
|
10241
|
+
params.push(...lineage);
|
|
10242
|
+
}
|
|
10243
|
+
if (kind)
|
|
10244
|
+
params.push(kind);
|
|
10245
|
+
if (key)
|
|
10246
|
+
params.push(key);
|
|
10247
|
+
return db.query(`
|
|
9546
10248
|
SELECT * FROM context_nodes
|
|
9547
10249
|
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);
|
|
10250
|
+
${branchClause} ${kind ? "AND kind = ?" : ""} ${key ? "AND fact_key = ?" : ""}
|
|
10251
|
+
`).all(...params);
|
|
9551
10252
|
}
|
|
9552
10253
|
function latestLineageFact(db, scope, kind, key) {
|
|
9553
10254
|
const rank = new Map(branchLineage(scope).map((id, index) => [id, index]));
|
|
@@ -9605,10 +10306,9 @@ function pruneProject(db, projectId) {
|
|
|
9605
10306
|
ORDER BY CASE WHEN status = 'active' THEN 1 ELSE 0 END, updated_at ASC
|
|
9606
10307
|
LIMIT ?
|
|
9607
10308
|
`).all(projectId, excess) : [];
|
|
9608
|
-
const removeFts = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9609
10309
|
const removeNode = db.query("DELETE FROM context_nodes WHERE id = ?");
|
|
9610
10310
|
for (const victim of victims) {
|
|
9611
|
-
|
|
10311
|
+
removeFtsNode(db, victim.id);
|
|
9612
10312
|
removeNode.run(victim.id);
|
|
9613
10313
|
}
|
|
9614
10314
|
const staleSessions = db.query(`
|
|
@@ -9619,6 +10319,7 @@ function pruneProject(db, projectId) {
|
|
|
9619
10319
|
for (const session of staleSessions)
|
|
9620
10320
|
removeNode.run(session.id);
|
|
9621
10321
|
}
|
|
10322
|
+
var activeCompactionIndexDatabase = null;
|
|
9622
10323
|
function indexCompactionState(projectId, state) {
|
|
9623
10324
|
const sessionId = state.scope?.sessionId;
|
|
9624
10325
|
if (!sessionId || state.scope?.projectId !== projectId)
|
|
@@ -9630,8 +10331,9 @@ function indexCompactionState(projectId, state) {
|
|
|
9630
10331
|
branchEntryIds: state.scope.branchAncestryIds
|
|
9631
10332
|
};
|
|
9632
10333
|
let db = null;
|
|
10334
|
+
const ownsDatabase = activeCompactionIndexDatabase === null;
|
|
9633
10335
|
try {
|
|
9634
|
-
db = openDatabase();
|
|
10336
|
+
db = activeCompactionIndexDatabase ?? openDatabase();
|
|
9635
10337
|
const transaction = db.transaction(() => {
|
|
9636
10338
|
const now = Date.now();
|
|
9637
10339
|
const projectNode = makeNode({ ...scope, sessionId: "*", branchHeadId: undefined }, "project", "Project", projectId, { confidence: 1 });
|
|
@@ -9709,9 +10411,11 @@ function indexCompactionState(projectId, state) {
|
|
|
9709
10411
|
warn("indexCompactionState failed", error2);
|
|
9710
10412
|
return false;
|
|
9711
10413
|
} finally {
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
10414
|
+
if (ownsDatabase) {
|
|
10415
|
+
try {
|
|
10416
|
+
db?.close();
|
|
10417
|
+
} catch {}
|
|
10418
|
+
}
|
|
9715
10419
|
}
|
|
9716
10420
|
}
|
|
9717
10421
|
var pendingCompactionIndexes = new Map;
|
|
@@ -9721,8 +10425,20 @@ function drainCompactionIndexes() {
|
|
|
9721
10425
|
compactionIndexTimer = null;
|
|
9722
10426
|
const jobs = [...pendingCompactionIndexes.values()];
|
|
9723
10427
|
pendingCompactionIndexes.clear();
|
|
9724
|
-
|
|
9725
|
-
|
|
10428
|
+
let db = null;
|
|
10429
|
+
try {
|
|
10430
|
+
db = openDatabase();
|
|
10431
|
+
activeCompactionIndexDatabase = db;
|
|
10432
|
+
for (const job of jobs)
|
|
10433
|
+
indexCompactionState(job.projectId, job.state);
|
|
10434
|
+
} catch (error2) {
|
|
10435
|
+
warn("context graph index drain failed", error2);
|
|
10436
|
+
} finally {
|
|
10437
|
+
activeCompactionIndexDatabase = null;
|
|
10438
|
+
try {
|
|
10439
|
+
db?.close();
|
|
10440
|
+
} catch {}
|
|
10441
|
+
}
|
|
9726
10442
|
if (pendingCompactionIndexes.size)
|
|
9727
10443
|
armCompactionIndexDrain();
|
|
9728
10444
|
}
|
|
@@ -9737,8 +10453,10 @@ function scheduleCompactionStateIndex(projectId, state) {
|
|
|
9737
10453
|
if (!sessionId || !branchHeadId || state.scope?.projectId !== projectId)
|
|
9738
10454
|
return false;
|
|
9739
10455
|
const key = projectId + "\x00" + sessionId + "\x00" + branchHeadId;
|
|
9740
|
-
if (
|
|
9741
|
-
|
|
10456
|
+
if (pendingCompactionIndexes.has(key))
|
|
10457
|
+
pendingCompactionIndexes.delete(key);
|
|
10458
|
+
if (pendingCompactionIndexes.size >= MAX_PENDING_COMPACTION_INDEXES) {
|
|
10459
|
+
warn("context graph index queue full; new derived update was rejected");
|
|
9742
10460
|
return false;
|
|
9743
10461
|
}
|
|
9744
10462
|
pendingCompactionIndexes.set(key, { projectId, state });
|
|
@@ -9760,9 +10478,8 @@ function closeContextMemory(projectId, kind, content, status) {
|
|
|
9760
10478
|
UPDATE context_nodes SET status = ?, updated_at = ?
|
|
9761
10479
|
WHERE project_id = ? AND kind = ? AND fact_key = ? AND source = 'manual' AND status = 'active'
|
|
9762
10480
|
`).run(status, Date.now(), projectId, kind, factKey(content));
|
|
9763
|
-
const remove = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9764
10481
|
for (const row of rows)
|
|
9765
|
-
|
|
10482
|
+
removeFtsNode(db, row.id);
|
|
9766
10483
|
});
|
|
9767
10484
|
transaction();
|
|
9768
10485
|
return rows.length;
|
|
@@ -9808,10 +10525,9 @@ function saveContextMemory(scope, memory) {
|
|
|
9808
10525
|
...duplicates.flatMap((item) => parsePaths(item.related_paths))
|
|
9809
10526
|
])).slice(0, 20);
|
|
9810
10527
|
upsertNode(db, node, true);
|
|
9811
|
-
const removeFts = db.query("DELETE FROM context_nodes_fts WHERE node_id = ?");
|
|
9812
10528
|
const removeNode = db.query("DELETE FROM context_nodes WHERE id = ?");
|
|
9813
10529
|
for (const duplicate of duplicates) {
|
|
9814
|
-
|
|
10530
|
+
removeFtsNode(db, duplicate.id);
|
|
9815
10531
|
removeNode.run(duplicate.id);
|
|
9816
10532
|
}
|
|
9817
10533
|
for (const file of relatedPaths) {
|
|
@@ -9838,7 +10554,7 @@ function searchRows(db, projectId, terms) {
|
|
|
9838
10554
|
try {
|
|
9839
10555
|
return db.query(`
|
|
9840
10556
|
SELECT n.* FROM context_nodes_fts f
|
|
9841
|
-
JOIN context_nodes n ON n.
|
|
10557
|
+
JOIN context_nodes n ON n.rowid = f.rowid
|
|
9842
10558
|
WHERE context_nodes_fts MATCH ? AND n.project_id = ? AND n.status = 'active'
|
|
9843
10559
|
AND n.kind NOT IN ('project', 'session')
|
|
9844
10560
|
ORDER BY bm25(context_nodes_fts, 0.0, 3.0, 1.0, 0.5)
|
|
@@ -10130,8 +10846,8 @@ function buildSuccessMetrics(rc, status) {
|
|
|
10130
10846
|
adapted: rc.adapted
|
|
10131
10847
|
};
|
|
10132
10848
|
}
|
|
10133
|
-
function recordSuccessMetrics(rc, status) {
|
|
10134
|
-
appendMetricsSnapshot(rc.sessionId, buildSuccessMetrics(rc, status));
|
|
10849
|
+
async function recordSuccessMetrics(rc, status) {
|
|
10850
|
+
await appendMetricsSnapshot(rc.sessionId, buildSuccessMetrics(rc, status));
|
|
10135
10851
|
const ecs = getExtractionCacheStats(rc.services);
|
|
10136
10852
|
const ms = getMetricsSummary(rc.services);
|
|
10137
10853
|
if (status === "success" && ms.totalCalls > 0) {
|
|
@@ -10142,7 +10858,7 @@ function recordSuccessMetrics(rc, status) {
|
|
|
10142
10858
|
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
10859
|
}
|
|
10144
10860
|
}
|
|
10145
|
-
function recordFailureMetrics(rc, err, fields) {
|
|
10861
|
+
async function recordFailureMetrics(rc, err, fields) {
|
|
10146
10862
|
const releaseChannel = rc.config?.telemetryChannel ?? loadConfig().telemetryChannel;
|
|
10147
10863
|
const failureKind = classifyTelemetryFailure(err, rc.cancellation.timedOut);
|
|
10148
10864
|
const gate = err && typeof err === "object" ? err : null;
|
|
@@ -10165,7 +10881,7 @@ function recordFailureMetrics(rc, err, fields) {
|
|
|
10165
10881
|
const verificationScore = typeof gate?.score === "number" && Number.isFinite(gate.score) ? gate.score : undefined;
|
|
10166
10882
|
const initialVerificationScore = typeof gate?.initialScore === "number" && Number.isFinite(gate.initialScore) ? gate.initialScore : undefined;
|
|
10167
10883
|
const verificationGaps = typeof gate?.gapCount === "number" && Number.isInteger(gate.gapCount) && gate.gapCount >= 0 ? gate.gapCount : undefined;
|
|
10168
|
-
appendMetricsLog(fields.sessionId ?? "unknown", {
|
|
10884
|
+
await appendMetricsLog(fields.sessionId ?? "unknown", {
|
|
10169
10885
|
runId: rc.runId,
|
|
10170
10886
|
metricsSchemaVersion: 2,
|
|
10171
10887
|
version: VERSION,
|
|
@@ -10252,7 +10968,7 @@ async function commitAppliedCompaction(pending) {
|
|
|
10252
10968
|
failures.push("conversation backup");
|
|
10253
10969
|
if (!pending.metricsSnapshot)
|
|
10254
10970
|
return failures;
|
|
10255
|
-
appendMetricsSnapshot(pending.sessionId, {
|
|
10971
|
+
await appendMetricsSnapshot(pending.sessionId, {
|
|
10256
10972
|
...pending.metricsSnapshot,
|
|
10257
10973
|
persistenceStatus: failures.length ? "partial" : "complete",
|
|
10258
10974
|
persistenceFailures: failures.length ? failures : undefined,
|
|
@@ -10289,10 +11005,14 @@ function runDamageDetection(rc) {
|
|
|
10289
11005
|
}
|
|
10290
11006
|
}
|
|
10291
11007
|
function stagePendingCompaction(rc, metricsSnapshot) {
|
|
11008
|
+
const originBranchHeadId = branchEntryIds(rc.branch).at(-1);
|
|
11009
|
+
if (!originBranchHeadId)
|
|
11010
|
+
throw new Error("Pending compaction requires an identifiable branch head");
|
|
10292
11011
|
const pending = {
|
|
10293
11012
|
runId: rc.runId,
|
|
10294
11013
|
summary: rc.finalSummary,
|
|
10295
11014
|
firstKeptEntryId: rc.firstKeptId,
|
|
11015
|
+
originBranchHeadId,
|
|
10296
11016
|
tokensBefore: rc.totalTokens,
|
|
10297
11017
|
details: rc.details,
|
|
10298
11018
|
metricsSnapshot,
|
|
@@ -10349,7 +11069,9 @@ function makeBase(opts) {
|
|
|
10349
11069
|
};
|
|
10350
11070
|
const pipelineStart = Date.now();
|
|
10351
11071
|
const requestedMode = opts.mode ?? modeFromLegacyProfile(opts.profile ?? "balanced");
|
|
10352
|
-
const
|
|
11072
|
+
const usage = opts.ctx.getContextUsage();
|
|
11073
|
+
const reportedPercent = usage?.percent;
|
|
11074
|
+
const contextPercent = Number.isFinite(reportedPercent) && (reportedPercent ?? 0) >= 0 ? reportedPercent : safeContextPercent(usage?.tokens, opts.ctx.model?.contextWindow);
|
|
10353
11075
|
const mode = resolveMode(requestedMode, contextPercent);
|
|
10354
11076
|
const profile = opts.mode ? MODE_POLICIES[mode].profile : opts.profile ?? MODE_POLICIES[mode].profile;
|
|
10355
11077
|
return {
|
|
@@ -10483,7 +11205,7 @@ async function runSmartCompact(opts) {
|
|
|
10483
11205
|
stated.vlog("Pipeline complete \u2014 method=" + stated.method + " calls=" + stated.llmCalls + " chunks=" + stated.chunkCount + " tokensSaved=" + stated.tokensSaved);
|
|
10484
11206
|
markPhase(stated, "state");
|
|
10485
11207
|
if (stated.flags.dryRun) {
|
|
10486
|
-
recordSuccessMetrics(stated, "dry-run");
|
|
11208
|
+
await recordSuccessMetrics(stated, "dry-run");
|
|
10487
11209
|
stated.ctx.ui.notify("DRY RUN (" + stated.method + ", " + stated.mode + ") \u2014 " + stated.toCompact.length + " msgs, " + stated.llmCalls + " calls", "info");
|
|
10488
11210
|
return { kind: "dry-run", details: stated.details };
|
|
10489
11211
|
}
|
|
@@ -10506,7 +11228,7 @@ async function runSmartCompact(opts) {
|
|
|
10506
11228
|
}
|
|
10507
11229
|
if (decision !== "apply") {
|
|
10508
11230
|
stated.pendingRef.clear(stated.sessionId);
|
|
10509
|
-
recordSuccessMetrics(stated, "cancelled");
|
|
11231
|
+
await recordSuccessMetrics(stated, "cancelled");
|
|
10510
11232
|
stated.ctx.ui.notify("Compaction cancelled \u2014 current conversation unchanged", "info");
|
|
10511
11233
|
return { kind: "cancelled", source: "user" };
|
|
10512
11234
|
}
|
|
@@ -10532,7 +11254,7 @@ async function runSmartCompact(opts) {
|
|
|
10532
11254
|
return willApply ? { kind: "apply-requested", pending } : { kind: "staged", pending };
|
|
10533
11255
|
} catch (err) {
|
|
10534
11256
|
runFailed = true;
|
|
10535
|
-
recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
11257
|
+
await recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
10536
11258
|
throw err;
|
|
10537
11259
|
} finally {
|
|
10538
11260
|
opts.abortSignal?.removeEventListener("abort", abortFromHost);
|
|
@@ -10725,55 +11447,70 @@ function parseSmartCompactTool(params) {
|
|
|
10725
11447
|
function createPendingSlot(opts) {
|
|
10726
11448
|
const ttlMs = opts.ttlMs;
|
|
10727
11449
|
const now = opts.now ?? Date.now;
|
|
10728
|
-
const maxEntries = Math.max(1, opts.maxEntries ??
|
|
11450
|
+
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
10729
11451
|
const entries = new Map;
|
|
11452
|
+
let newestSessionId = null;
|
|
11453
|
+
const refreshNewest = () => {
|
|
11454
|
+
newestSessionId = null;
|
|
11455
|
+
for (const sessionId of entries.keys())
|
|
11456
|
+
newestSessionId = sessionId;
|
|
11457
|
+
};
|
|
11458
|
+
const deleteEntry = (sessionId) => {
|
|
11459
|
+
if (!entries.delete(sessionId))
|
|
11460
|
+
return;
|
|
11461
|
+
if (newestSessionId === sessionId)
|
|
11462
|
+
refreshNewest();
|
|
11463
|
+
};
|
|
10730
11464
|
const prune = () => {
|
|
10731
|
-
const
|
|
11465
|
+
const current = now();
|
|
11466
|
+
let removedNewest = false;
|
|
10732
11467
|
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;
|
|
11468
|
+
if (current - entry.createdAt <= ttlMs)
|
|
11469
|
+
continue;
|
|
11470
|
+
entries.delete(sessionId);
|
|
11471
|
+
if (newestSessionId === sessionId)
|
|
11472
|
+
removedNewest = true;
|
|
10742
11473
|
}
|
|
10743
|
-
|
|
11474
|
+
if (removedNewest)
|
|
11475
|
+
refreshNewest();
|
|
10744
11476
|
};
|
|
10745
11477
|
return {
|
|
10746
11478
|
set(pending) {
|
|
10747
11479
|
prune();
|
|
10748
11480
|
entries.delete(pending.sessionId);
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
11481
|
+
entries.set(pending.sessionId, { value: pending, createdAt: now() });
|
|
11482
|
+
newestSessionId = pending.sessionId;
|
|
11483
|
+
while (entries.size > maxEntries) {
|
|
11484
|
+
const oldest = entries.keys().next().value;
|
|
11485
|
+
if (oldest === undefined)
|
|
10752
11486
|
break;
|
|
10753
|
-
|
|
11487
|
+
deleteEntry(oldest);
|
|
10754
11488
|
}
|
|
10755
|
-
entries.set(pending.sessionId, { value: pending, createdAt: now() });
|
|
10756
11489
|
},
|
|
10757
11490
|
consume(ctx) {
|
|
10758
11491
|
const currentSessionId = resolveSessionId(ctx);
|
|
10759
11492
|
const entry = entries.get(currentSessionId);
|
|
10760
|
-
if (
|
|
10761
|
-
const
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
11493
|
+
if (entry) {
|
|
11494
|
+
const ageMs = now() - entry.createdAt;
|
|
11495
|
+
if (ageMs > ttlMs) {
|
|
11496
|
+
deleteEntry(currentSessionId);
|
|
11497
|
+
prune();
|
|
11498
|
+
return { kind: "expired", ageMs };
|
|
11499
|
+
}
|
|
11500
|
+
deleteEntry(currentSessionId);
|
|
11501
|
+
return { kind: "ok", pending: entry.value };
|
|
10768
11502
|
}
|
|
10769
|
-
|
|
10770
|
-
|
|
11503
|
+
prune();
|
|
11504
|
+
const other = newestSessionId == null ? undefined : entries.get(newestSessionId);
|
|
11505
|
+
return other ? { kind: "mismatch", expected: other.value.sessionId, actual: currentSessionId } : { kind: "empty" };
|
|
10771
11506
|
},
|
|
10772
11507
|
clear(sessionId) {
|
|
10773
11508
|
if (sessionId)
|
|
10774
|
-
|
|
10775
|
-
else
|
|
11509
|
+
deleteEntry(sessionId);
|
|
11510
|
+
else {
|
|
10776
11511
|
entries.clear();
|
|
11512
|
+
newestSessionId = null;
|
|
11513
|
+
}
|
|
10777
11514
|
},
|
|
10778
11515
|
isPresent(sessionId) {
|
|
10779
11516
|
prune();
|
|
@@ -10781,7 +11518,8 @@ function createPendingSlot(opts) {
|
|
|
10781
11518
|
},
|
|
10782
11519
|
peek(sessionId) {
|
|
10783
11520
|
prune();
|
|
10784
|
-
|
|
11521
|
+
const entry = sessionId ? entries.get(sessionId) : newestSessionId == null ? undefined : entries.get(newestSessionId);
|
|
11522
|
+
return entry?.value ?? null;
|
|
10785
11523
|
},
|
|
10786
11524
|
size() {
|
|
10787
11525
|
prune();
|
|
@@ -10861,18 +11599,30 @@ function createCompactionCommitStore(options = {}) {
|
|
|
10861
11599
|
// src/app/native-continuity-bridge.ts
|
|
10862
11600
|
import crypto6 from "crypto";
|
|
10863
11601
|
import fs10 from "fs";
|
|
10864
|
-
import
|
|
11602
|
+
import path14 from "path";
|
|
10865
11603
|
var MAX_TEXT_BYTES = 256 * 1024;
|
|
10866
11604
|
function sameScope(a, b) {
|
|
10867
11605
|
return a.projectId === b.projectId && a.sessionId === b.sessionId && a.branchHeadId === b.branchHeadId;
|
|
10868
11606
|
}
|
|
11607
|
+
function boundedContinuityText(text) {
|
|
11608
|
+
const bytes = Buffer.from(text);
|
|
11609
|
+
if (bytes.length <= MAX_TEXT_BYTES)
|
|
11610
|
+
return text;
|
|
11611
|
+
const marker = Buffer.from(`
|
|
11612
|
+
\u2026 [continuity truncated from ` + bytes.length + ` bytes]
|
|
11613
|
+
`);
|
|
11614
|
+
let end = Math.max(0, MAX_TEXT_BYTES - marker.length);
|
|
11615
|
+
while (end > 0 && (bytes[end] & 192) === 128)
|
|
11616
|
+
end--;
|
|
11617
|
+
return Buffer.concat([bytes.subarray(0, end), marker]).toString("utf8");
|
|
11618
|
+
}
|
|
10869
11619
|
function createNativeContinuityBridge(opts = {}) {
|
|
10870
11620
|
const ttlMs = Math.max(1, opts.ttlMs ?? SEVEN_DAYS_MS);
|
|
10871
11621
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
10872
11622
|
const now = opts.now ?? Date.now;
|
|
10873
11623
|
const dir = opts.dir ?? nativeContinuityDir();
|
|
10874
|
-
const lockTarget =
|
|
10875
|
-
const fileFor = (scope) =>
|
|
11624
|
+
const lockTarget = path14.join(dir, "bridge");
|
|
11625
|
+
const fileFor = (scope) => path14.join(dir, crypto6.createHash("sha256").update(scope.projectId + "\x00" + scope.sessionId + "\x00" + scope.branchHeadId).digest("hex") + ".json");
|
|
10876
11626
|
const validScope = (scope) => Boolean(scope.projectId && scope.sessionId && scope.branchHeadId);
|
|
10877
11627
|
const readEntry = (file) => {
|
|
10878
11628
|
try {
|
|
@@ -10897,7 +11647,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10897
11647
|
for (const name of names) {
|
|
10898
11648
|
if (!/\.tmp\.\d+\.[0-9a-f]+$/i.test(name))
|
|
10899
11649
|
continue;
|
|
10900
|
-
const file =
|
|
11650
|
+
const file = path14.join(dir, name);
|
|
10901
11651
|
try {
|
|
10902
11652
|
if (now() - fs10.statSync(file).mtimeMs > ONE_HOUR_MS)
|
|
10903
11653
|
fs10.unlinkSync(file);
|
|
@@ -10905,7 +11655,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10905
11655
|
}
|
|
10906
11656
|
const files = names.filter((file) => file.endsWith(".json"));
|
|
10907
11657
|
for (const name of files) {
|
|
10908
|
-
const file =
|
|
11658
|
+
const file = path14.join(dir, name);
|
|
10909
11659
|
const entry = readEntry(file);
|
|
10910
11660
|
if (!entry || now() - entry.createdAt > ttlMs || entry.createdAt - now() > ttlMs) {
|
|
10911
11661
|
try {
|
|
@@ -10939,8 +11689,9 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10939
11689
|
};
|
|
10940
11690
|
return {
|
|
10941
11691
|
stage(scope, text) {
|
|
10942
|
-
if (!validScope(scope) || !text.trim()
|
|
11692
|
+
if (!validScope(scope) || !text.trim())
|
|
10943
11693
|
return;
|
|
11694
|
+
const boundedText = boundedContinuityText(text);
|
|
10944
11695
|
try {
|
|
10945
11696
|
locked(() => {
|
|
10946
11697
|
const target = fileFor(scope);
|
|
@@ -10948,7 +11699,7 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
10948
11699
|
fs10.unlinkSync(target);
|
|
10949
11700
|
} catch {}
|
|
10950
11701
|
prune(1);
|
|
10951
|
-
const entry = { schemaVersion: 1, scope, text, createdAt: now() };
|
|
11702
|
+
const entry = { schemaVersion: 1, scope, text: boundedText, createdAt: now() };
|
|
10952
11703
|
atomicWriteFileSync(target, JSON.stringify(entry));
|
|
10953
11704
|
try {
|
|
10954
11705
|
fs10.chmodSync(target, 384);
|
|
@@ -11013,8 +11764,15 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
11013
11764
|
// src/index.ts
|
|
11014
11765
|
function unwrapConsumed(result, ctx) {
|
|
11015
11766
|
switch (result.kind) {
|
|
11016
|
-
case "ok":
|
|
11767
|
+
case "ok": {
|
|
11768
|
+
const activeEntryIds = new Set(branchEntryIds(ctx.sessionManager.getBranch()));
|
|
11769
|
+
if (!activeEntryIds.has(result.pending.originBranchHeadId) || !activeEntryIds.has(result.pending.firstKeptEntryId)) {
|
|
11770
|
+
warn("Discarding pending smart compaction prepared for a divergent branch");
|
|
11771
|
+
ctx.ui.notify("Divergent-branch pending smart compaction discarded", "warning");
|
|
11772
|
+
return null;
|
|
11773
|
+
}
|
|
11017
11774
|
return result.pending;
|
|
11775
|
+
}
|
|
11018
11776
|
case "empty":
|
|
11019
11777
|
return null;
|
|
11020
11778
|
case "expired":
|
|
@@ -11181,7 +11939,7 @@ function smartCompactExtension(pi) {
|
|
|
11181
11939
|
const scrubber = new SecretScrubber(config.scrubSecrets, config.scrubPii);
|
|
11182
11940
|
const title = scrubber.scrubText(params.title?.trim() || "Saved " + params.kind).value;
|
|
11183
11941
|
const content = scrubber.scrubText(params.content).value;
|
|
11184
|
-
const relatedPaths = (params.related_paths ?? []).map((
|
|
11942
|
+
const relatedPaths = (params.related_paths ?? []).map((path15) => scrubber.scrubText(path15).value);
|
|
11185
11943
|
const status = params.status ?? "active";
|
|
11186
11944
|
if (!ctx.hasUI) {
|
|
11187
11945
|
return { content: [{ type: "text", text: "Project memory requires an interactive host confirmation; nothing changed." }], details: undefined };
|
|
@@ -11219,7 +11977,10 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11219
11977
|
await ctx.waitForIdle();
|
|
11220
11978
|
try {
|
|
11221
11979
|
const knownProviders = new Set(ctx.modelRegistry.getAvailable().map((model) => model.provider));
|
|
11222
|
-
const parsedInput = parseSmartCompactCommand(args, (token) =>
|
|
11980
|
+
const parsedInput = parseSmartCompactCommand(args, (token) => {
|
|
11981
|
+
const [provider, ...modelPath] = token.split("/");
|
|
11982
|
+
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));
|
|
11983
|
+
});
|
|
11223
11984
|
if (!parsedInput.ok) {
|
|
11224
11985
|
ctx.ui.notify(parsedInput.error, "error");
|
|
11225
11986
|
return;
|
|
@@ -11366,7 +12127,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11366
12127
|
if (!args.trim()) {
|
|
11367
12128
|
const usage = ctx.getContextUsage();
|
|
11368
12129
|
const totalTokens = usage?.tokens ?? 0;
|
|
11369
|
-
const pct =
|
|
12130
|
+
const pct = Math.round(safeContextPercent(totalTokens, ctx.model?.contextWindow));
|
|
11370
12131
|
const cur = ctx.model;
|
|
11371
12132
|
const initialRoutes = resolveModels(ctx, cur, config);
|
|
11372
12133
|
if (!initialRoutes.sumModel) {
|
|
@@ -11442,7 +12203,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11442
12203
|
const totalTokens = usage?.tokens ?? 0;
|
|
11443
12204
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
|
|
11444
12205
|
return;
|
|
11445
|
-
const pct =
|
|
12206
|
+
const pct = safeContextPercent(totalTokens, ctx.model?.contextWindow);
|
|
11446
12207
|
if (event.reason !== "overflow" && pct < config.minContextPercent)
|
|
11447
12208
|
return;
|
|
11448
12209
|
const cur = ctx.model;
|
|
@@ -11475,6 +12236,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
11475
12236
|
overflowRecovery: event.reason === "overflow",
|
|
11476
12237
|
maxLlmCalls: Math.min(config.maxLlmCalls, AUTO_TRIGGER_MAX_LLM_CALLS),
|
|
11477
12238
|
timeoutMs: effectiveTimeoutMs,
|
|
12239
|
+
abortSignal: event.signal,
|
|
11478
12240
|
cancellationOut
|
|
11479
12241
|
});
|
|
11480
12242
|
} catch (err) {
|
|
@@ -11635,12 +12397,12 @@ Dashboard: ` + fp : "") }], details: undefined };
|
|
|
11635
12397
|
}
|
|
11636
12398
|
const usage = ctx.getContextUsage?.();
|
|
11637
12399
|
const totalTokens = usage?.tokens ?? 0;
|
|
11638
|
-
const
|
|
11639
|
-
const pct = Math.round(
|
|
12400
|
+
const contextPercent = safeContextPercent(totalTokens, ctx.model?.contextWindow);
|
|
12401
|
+
const pct = Math.round(contextPercent);
|
|
11640
12402
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
11641
12403
|
return { content: [{ type: "text", text: "Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + pct + "%). No action needed." }], details: undefined };
|
|
11642
12404
|
}
|
|
11643
|
-
if (
|
|
12405
|
+
if (contextPercent < config.minContextPercent) {
|
|
11644
12406
|
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
12407
|
}
|
|
11646
12408
|
const cur = ctx.model;
|