pi-smart-compact 9.6.0 → 9.6.2
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 +28 -14
- package/CHANGELOG.md +46 -0
- package/README.md +30 -10
- package/dist/app/mode-policy.d.ts +3 -1
- package/dist/app/mode-policy.d.ts.map +1 -1
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/register-smart-compact-tool.d.ts.map +1 -1
- package/dist/app/run-context.d.ts +3 -3
- package/dist/app/run-context.d.ts.map +1 -1
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/prepare.d.ts.map +1 -1
- package/dist/app/steps/state.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/app/steps/tier.d.ts +5 -4
- package/dist/app/steps/tier.d.ts.map +1 -1
- package/dist/app/steps/window.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/domain/provider-evaluation.d.ts.map +1 -1
- package/dist/domain/telemetry.d.ts.map +1 -1
- package/dist/domain/tool-semantics.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1489 -1248
- package/dist/infra/ai-messages.d.ts +6 -2
- package/dist/infra/ai-messages.d.ts.map +1 -1
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts +5 -0
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/provider-eval.js +348 -22
- package/dist/provider-scenario-eval.js +363 -33
- package/dist/telemetry-report.js +125 -22
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/error-format.d.ts +3 -1
- package/dist/ui/error-format.d.ts.map +1 -1
- package/dist/ui/metrics-report.d.ts.map +1 -1
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/file-needles.d.ts +24 -0
- package/dist/utils/file-needles.d.ts.map +1 -1
- package/dist/utils/file-ref-detect.d.ts +6 -1
- package/dist/utils/file-ref-detect.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ var __require = import.meta.require;
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import {
|
|
6
|
-
convertToLlm as
|
|
6
|
+
convertToLlm as convertToLlm5
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
-
var VERSION = "9.6.
|
|
10
|
+
var VERSION = "9.6.2";
|
|
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;
|
|
@@ -1455,38 +1455,117 @@ function buildPathNeedles(filePath) {
|
|
|
1455
1455
|
}
|
|
1456
1456
|
return needles;
|
|
1457
1457
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1458
|
+
var MAX_INDEXED_SUFFIX_CHARS = 1024;
|
|
1459
|
+
function buildPathNeedleOwnershipIndex(allPaths) {
|
|
1460
|
+
const owners = new Map;
|
|
1461
|
+
const normalizedPaths = allPaths.map(normalizePath);
|
|
1462
|
+
let hasUnindexedSuffixes = false;
|
|
1463
|
+
for (const normalized of normalizedPaths) {
|
|
1464
|
+
const suffixes = new Set([normalized]);
|
|
1465
|
+
for (let index = 0;index < normalized.length; index++) {
|
|
1466
|
+
if (normalized[index] !== "/")
|
|
1467
|
+
continue;
|
|
1468
|
+
if (normalized.length - index - 1 <= MAX_INDEXED_SUFFIX_CHARS) {
|
|
1469
|
+
suffixes.add(normalized.slice(index + 1));
|
|
1470
|
+
} else {
|
|
1471
|
+
hasUnindexedSuffixes = true;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const suffix of suffixes) {
|
|
1475
|
+
owners.set(suffix, (owners.get(suffix) ?? 0) + 1);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return { counts: owners, normalizedPaths, hasUnindexedSuffixes };
|
|
1479
|
+
}
|
|
1480
|
+
function buildUniquePathNeedlesFromIndex(filePath, owners) {
|
|
1460
1481
|
return buildPathNeedles(filePath).filter((needle) => {
|
|
1461
|
-
|
|
1462
|
-
|
|
1482
|
+
if (!owners.hasUnindexedSuffixes)
|
|
1483
|
+
return owners.counts.get(needle) === 1;
|
|
1484
|
+
let count = 0;
|
|
1485
|
+
for (const candidate of owners.normalizedPaths) {
|
|
1463
1486
|
if (candidate === needle || candidate.endsWith("/" + needle))
|
|
1464
|
-
|
|
1465
|
-
if (
|
|
1487
|
+
count++;
|
|
1488
|
+
if (count > 1)
|
|
1466
1489
|
return false;
|
|
1467
1490
|
}
|
|
1468
|
-
return
|
|
1491
|
+
return count === 1;
|
|
1469
1492
|
});
|
|
1470
1493
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1494
|
+
var PATH_CANDIDATE_CHAR_RE = /[\w./-]/;
|
|
1495
|
+
function buildKnownPathReferenceIndex(knownPaths) {
|
|
1496
|
+
const segmentSuffixes = new Set;
|
|
1497
|
+
const boundarySuffixes = new Set;
|
|
1498
|
+
const normalizedPaths = [];
|
|
1499
|
+
let hasUnindexedSuffixes = false;
|
|
1500
|
+
for (const path3 of knownPaths) {
|
|
1477
1501
|
const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
|
|
1502
|
+
if (!normalizedPath)
|
|
1503
|
+
continue;
|
|
1504
|
+
normalizedPaths.push(normalizedPath);
|
|
1505
|
+
segmentSuffixes.add(normalizedPath);
|
|
1506
|
+
for (let index = 0;index < normalizedPath.length; index++) {
|
|
1507
|
+
if (normalizedPath[index] === "/") {
|
|
1508
|
+
if (normalizedPath.length - index - 1 <= MAX_INDEXED_SUFFIX_CHARS) {
|
|
1509
|
+
segmentSuffixes.add(normalizedPath.slice(index + 1));
|
|
1510
|
+
} else {
|
|
1511
|
+
hasUnindexedSuffixes = true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (index > 0 && !PATH_CANDIDATE_CHAR_RE.test(normalizedPath[index - 1])) {
|
|
1515
|
+
if (normalizedPath.length - index <= MAX_INDEXED_SUFFIX_CHARS) {
|
|
1516
|
+
boundarySuffixes.add(normalizedPath.slice(index));
|
|
1517
|
+
} else {
|
|
1518
|
+
hasUnindexedSuffixes = true;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
segmentSuffixes,
|
|
1525
|
+
sortedSegmentSuffixes: [...segmentSuffixes].sort(),
|
|
1526
|
+
boundarySuffixes,
|
|
1527
|
+
normalizedPaths,
|
|
1528
|
+
hasUnindexedSuffixes
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function matchesKnownPathReference(normalizedRef, normalizedPaths) {
|
|
1532
|
+
const pathShaped = normalizedRef.includes("/");
|
|
1533
|
+
return normalizedPaths.some((normalizedPath) => {
|
|
1478
1534
|
if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
|
|
1479
1535
|
return true;
|
|
1480
1536
|
if (normalizedPath.endsWith(normalizedRef)) {
|
|
1481
1537
|
const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
|
|
1482
|
-
if (boundary &&
|
|
1538
|
+
if (boundary && !PATH_CANDIDATE_CHAR_RE.test(boundary))
|
|
1483
1539
|
return true;
|
|
1484
1540
|
}
|
|
1485
1541
|
if (!pathShaped)
|
|
1486
1542
|
return false;
|
|
1487
|
-
return normalizedPath.
|
|
1543
|
+
return normalizedPath.startsWith(normalizedRef + "/") || normalizedPath.includes("/" + normalizedRef + "/");
|
|
1488
1544
|
});
|
|
1489
1545
|
}
|
|
1546
|
+
function sortedHasPrefix(values, prefix) {
|
|
1547
|
+
let low = 0;
|
|
1548
|
+
let high = values.length;
|
|
1549
|
+
while (low < high) {
|
|
1550
|
+
const middle = low + high >>> 1;
|
|
1551
|
+
if (values[middle] < prefix)
|
|
1552
|
+
low = middle + 1;
|
|
1553
|
+
else
|
|
1554
|
+
high = middle;
|
|
1555
|
+
}
|
|
1556
|
+
return values[low]?.startsWith(prefix) ?? false;
|
|
1557
|
+
}
|
|
1558
|
+
function isKnownPathReferenceInIndex(ref, index) {
|
|
1559
|
+
const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
|
|
1560
|
+
if (!normalizedRef)
|
|
1561
|
+
return false;
|
|
1562
|
+
if (index.segmentSuffixes.has(normalizedRef) || index.boundarySuffixes.has(normalizedRef)) {
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
if (normalizedRef.includes("/") && sortedHasPrefix(index.sortedSegmentSuffixes, normalizedRef + "/"))
|
|
1566
|
+
return true;
|
|
1567
|
+
return index.hasUnindexedSuffixes ? matchesKnownPathReference(normalizedRef, index.normalizedPaths) : false;
|
|
1568
|
+
}
|
|
1490
1569
|
|
|
1491
1570
|
// src/domain/tool-semantics.ts
|
|
1492
1571
|
var PATH_KEYS = [
|
|
@@ -1704,8 +1783,12 @@ function classifyToolOperation(args, toolName) {
|
|
|
1704
1783
|
function stableValue(value) {
|
|
1705
1784
|
if (Array.isArray(value))
|
|
1706
1785
|
return value.map(stableValue);
|
|
1707
|
-
if (
|
|
1786
|
+
if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1708
1787
|
return value;
|
|
1788
|
+
if (typeof value === "bigint")
|
|
1789
|
+
return value.toString();
|
|
1790
|
+
if (typeof value !== "object")
|
|
1791
|
+
return;
|
|
1709
1792
|
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
|
|
1710
1793
|
}
|
|
1711
1794
|
function commandIdentity(args) {
|
|
@@ -1778,16 +1861,8 @@ function findLastAnchorIndex(branchEntries) {
|
|
|
1778
1861
|
return -1;
|
|
1779
1862
|
}
|
|
1780
1863
|
function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const e = branchEntries[i];
|
|
1784
|
-
if (e?.type === "message") {
|
|
1785
|
-
if (msgCount >= msgs.length)
|
|
1786
|
-
return msgs.length - 1;
|
|
1787
|
-
msgCount++;
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
|
|
1864
|
+
const id = branchEntries[branchIdx]?.id;
|
|
1865
|
+
return Math.max(0, msgs.findIndex((entry) => entry.id === id));
|
|
1791
1866
|
}
|
|
1792
1867
|
function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
|
|
1793
1868
|
const candidates = [];
|
|
@@ -2091,7 +2166,12 @@ function buildExplorationContext(report) {
|
|
|
2091
2166
|
// src/utils/file-ref-detect.ts
|
|
2092
2167
|
var CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|rb|cs|cpp|c|h|hpp|swift|kt|scala|php|css|scss|html|json|yaml|yml|toml|md|mdx|sh|sql|tf|ini|env|lock|gradle|xml)$/i;
|
|
2093
2168
|
var VERSION_RE = /^v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/i;
|
|
2094
|
-
|
|
2169
|
+
function isAsciiWordCode(code) {
|
|
2170
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
|
|
2171
|
+
}
|
|
2172
|
+
function isCandidateCode(code) {
|
|
2173
|
+
return isAsciiWordCode(code) || code === 45 || code === 46 || code === 47;
|
|
2174
|
+
}
|
|
2095
2175
|
function isLikelyFileRef(candidate) {
|
|
2096
2176
|
if (candidate.startsWith("//") || VERSION_RE.test(candidate))
|
|
2097
2177
|
return false;
|
|
@@ -2102,13 +2182,32 @@ function isLikelyFileRef(candidate) {
|
|
|
2102
2182
|
return CODE_EXT_RE.test(candidate);
|
|
2103
2183
|
}
|
|
2104
2184
|
function extractFileRefs(summary) {
|
|
2105
|
-
const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
|
|
2106
2185
|
const refs = [];
|
|
2107
|
-
|
|
2108
|
-
|
|
2186
|
+
let cursor = 0;
|
|
2187
|
+
while (cursor < summary.length) {
|
|
2188
|
+
while (cursor < summary.length && !isCandidateCode(summary.charCodeAt(cursor)))
|
|
2189
|
+
cursor++;
|
|
2190
|
+
const runStart = cursor;
|
|
2191
|
+
while (cursor < summary.length && isCandidateCode(summary.charCodeAt(cursor)))
|
|
2192
|
+
cursor++;
|
|
2193
|
+
const runEnd = cursor;
|
|
2194
|
+
let extensionDot = -1;
|
|
2195
|
+
for (let index = runStart + 1;index + 1 < runEnd; index++) {
|
|
2196
|
+
if (summary.charCodeAt(index) === 46 && isAsciiWordCode(summary.charCodeAt(index + 1))) {
|
|
2197
|
+
extensionDot = index;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (extensionDot < 0)
|
|
2109
2201
|
continue;
|
|
2110
|
-
|
|
2111
|
-
|
|
2202
|
+
let matchEnd = extensionDot + 2;
|
|
2203
|
+
while (matchEnd < runEnd && isAsciiWordCode(summary.charCodeAt(matchEnd))) {
|
|
2204
|
+
matchEnd++;
|
|
2205
|
+
}
|
|
2206
|
+
const candidate = summary.slice(runStart, matchEnd);
|
|
2207
|
+
if (/[\\/]/.test(summary[matchEnd] ?? ""))
|
|
2208
|
+
continue;
|
|
2209
|
+
if (isLikelyFileRef(candidate))
|
|
2210
|
+
refs.push(candidate);
|
|
2112
2211
|
}
|
|
2113
2212
|
return refs;
|
|
2114
2213
|
}
|
|
@@ -3709,6 +3808,190 @@ function getDefaultServices() {
|
|
|
3709
3808
|
return _default;
|
|
3710
3809
|
}
|
|
3711
3810
|
|
|
3811
|
+
// src/domain/telemetry.ts
|
|
3812
|
+
function errorFields(error2, seen = new Set) {
|
|
3813
|
+
if (!error2 || typeof error2 !== "object") {
|
|
3814
|
+
return { name: "", message: String(error2 ?? ""), status: null, code: "" };
|
|
3815
|
+
}
|
|
3816
|
+
if (seen.has(error2) || seen.size >= 8)
|
|
3817
|
+
return { name: "", message: "", status: null, code: "" };
|
|
3818
|
+
seen.add(error2);
|
|
3819
|
+
const value = error2;
|
|
3820
|
+
const cause = value.cause ? errorFields(value.cause, seen) : null;
|
|
3821
|
+
const numericStatus = Number(value.status ?? value.statusCode);
|
|
3822
|
+
return {
|
|
3823
|
+
name: typeof value.name === "string" ? value.name : cause?.name ?? "",
|
|
3824
|
+
message: (typeof value.message === "string" ? value.message : "") + (cause?.message ? " " + cause.message : ""),
|
|
3825
|
+
status: Number.isFinite(numericStatus) ? numericStatus : cause?.status ?? null,
|
|
3826
|
+
code: typeof value.code === "string" ? value.code : cause?.code ?? ""
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
function classifyTelemetryFailure(error2, timedOut = false) {
|
|
3830
|
+
const fields = errorFields(error2);
|
|
3831
|
+
const text = (fields.name + " " + fields.code + " " + fields.message).toLowerCase();
|
|
3832
|
+
if (timedOut)
|
|
3833
|
+
return "timeout";
|
|
3834
|
+
if (/max(?:imum)? output|output.?limit|visible[ -]output|length limit/.test(text))
|
|
3835
|
+
return "output-limit";
|
|
3836
|
+
if (/timeout|timed out|watchdog|deadline/.test(text))
|
|
3837
|
+
return "timeout";
|
|
3838
|
+
if (fields.name.toLowerCase() === "verificationgateerror")
|
|
3839
|
+
return "verification";
|
|
3840
|
+
if (fields.name.toLowerCase() === "yieldgateerror")
|
|
3841
|
+
return "yield";
|
|
3842
|
+
if (/budgetexceeded|token budget|call budget|latency budget/.test(text))
|
|
3843
|
+
return "budget";
|
|
3844
|
+
if (fields.status === 429 || /rate.?limit|too many requests|quota/.test(text))
|
|
3845
|
+
return "rate-limit";
|
|
3846
|
+
if (fields.status === 401 || fields.status === 403 || /unauthori[sz]ed|authentication|api.?key|credential/.test(text))
|
|
3847
|
+
return "authentication";
|
|
3848
|
+
if (/abort|cancel/.test(text))
|
|
3849
|
+
return "cancelled";
|
|
3850
|
+
if (/native compaction|persist|write|rename|filesystem|sqlite|database/.test(text))
|
|
3851
|
+
return "persistence";
|
|
3852
|
+
if (/verificationgateerror|verification gate|verification.*(?:gap|summary)/.test(text))
|
|
3853
|
+
return "verification";
|
|
3854
|
+
if (/invalid|validation|schema|malformed|required/.test(text))
|
|
3855
|
+
return "validation";
|
|
3856
|
+
if (fields.status != null && fields.status >= 500 || /provider|api error|stream|network|fetch failed|socket/.test(text))
|
|
3857
|
+
return "provider";
|
|
3858
|
+
return "internal";
|
|
3859
|
+
}
|
|
3860
|
+
function p95(values) {
|
|
3861
|
+
if (!values.length)
|
|
3862
|
+
return 0;
|
|
3863
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3864
|
+
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
|
|
3865
|
+
}
|
|
3866
|
+
function stats(entries, damage) {
|
|
3867
|
+
const evidence = entries.filter((entry) => entry.status !== "dry-run");
|
|
3868
|
+
const successfulRuns = evidence.filter((entry) => entry.status === "success");
|
|
3869
|
+
const quality = successfulRuns.filter((entry) => typeof entry.verificationScore === "number");
|
|
3870
|
+
const appliedRunIds = new Set(successfulRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
|
|
3871
|
+
const observedScores = new Map;
|
|
3872
|
+
for (const observation of damage) {
|
|
3873
|
+
if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
|
|
3874
|
+
continue;
|
|
3875
|
+
observedScores.set(observation.runId, Math.max(observedScores.get(observation.runId) ?? 0, Math.max(0, Math.min(100, observation.damageScore))));
|
|
3876
|
+
}
|
|
3877
|
+
const damaging = [...observedScores.values()].filter((score) => score > 0).length;
|
|
3878
|
+
return {
|
|
3879
|
+
runs: entries.length,
|
|
3880
|
+
appliedRuns: evidence.length,
|
|
3881
|
+
successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
|
|
3882
|
+
avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
|
|
3883
|
+
qualityCoverage: evidence.length ? quality.length / evidence.length : 0,
|
|
3884
|
+
p95LatencyMs: p95(evidence.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
|
|
3885
|
+
avgTokens: evidence.length ? evidence.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / evidence.length : 0,
|
|
3886
|
+
fallbackRate: evidence.length ? evidence.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / evidence.length : 0,
|
|
3887
|
+
damageRate: observedScores.size ? damaging / observedScores.size : 0,
|
|
3888
|
+
damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
function roundStats(value) {
|
|
3892
|
+
return {
|
|
3893
|
+
...value,
|
|
3894
|
+
successRate: Math.round(value.successRate * 1000) / 1000,
|
|
3895
|
+
avgQuality: value.avgQuality == null ? null : Math.round(value.avgQuality * 10) / 10,
|
|
3896
|
+
qualityCoverage: Math.round(value.qualityCoverage * 1000) / 1000,
|
|
3897
|
+
p95LatencyMs: Math.round(value.p95LatencyMs),
|
|
3898
|
+
avgTokens: Math.round(value.avgTokens),
|
|
3899
|
+
fallbackRate: Math.round(value.fallbackRate * 1000) / 1000,
|
|
3900
|
+
damageRate: Math.round(value.damageRate * 1000) / 1000,
|
|
3901
|
+
damageCoverage: Math.round(value.damageCoverage * 1000) / 1000
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
function assessCanary(entries, damageEntries, options) {
|
|
3905
|
+
const minCanaryRuns = Math.max(5, options.minCanaryRuns ?? 20);
|
|
3906
|
+
const canaryEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && entry.version === options.version && entry.releaseChannel === "canary").slice(-Math.max(100, minCanaryRuns));
|
|
3907
|
+
const baselineEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && (entry.releaseChannel ?? "stable") === "stable").slice(-(options.baselineRuns ?? Math.max(50, minCanaryRuns * 2)));
|
|
3908
|
+
const baseline = stats(baselineEntries, damageEntries);
|
|
3909
|
+
const canary = stats(canaryEntries, damageEntries);
|
|
3910
|
+
const triggers = [];
|
|
3911
|
+
const failureBaseline = 1 - baseline.successRate;
|
|
3912
|
+
const failureCanary = 1 - canary.successRate;
|
|
3913
|
+
if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
|
|
3914
|
+
triggers.push({
|
|
3915
|
+
metric: "failure-rate",
|
|
3916
|
+
baseline: failureBaseline,
|
|
3917
|
+
canary: failureCanary,
|
|
3918
|
+
threshold: failureCanary > 0.050001 ? ">5% absolute" : "+5pp regression"
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
if (canary.avgQuality != null && (canary.avgQuality < 85 || baseline.avgQuality != null && baseline.avgQuality - canary.avgQuality >= 5)) {
|
|
3922
|
+
triggers.push({
|
|
3923
|
+
metric: "quality",
|
|
3924
|
+
baseline: baseline.avgQuality ?? 0,
|
|
3925
|
+
canary: canary.avgQuality,
|
|
3926
|
+
threshold: canary.avgQuality < 85 ? "<85 absolute" : "-5 points"
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
if (baseline.p95LatencyMs >= 1000 && canary.p95LatencyMs >= baseline.p95LatencyMs * 1.5) {
|
|
3930
|
+
triggers.push({ metric: "latency", baseline: baseline.p95LatencyMs, canary: canary.p95LatencyMs, threshold: "+50% p95" });
|
|
3931
|
+
}
|
|
3932
|
+
if (baseline.avgTokens >= 1000 && canary.avgTokens >= baseline.avgTokens * 1.5) {
|
|
3933
|
+
triggers.push({ metric: "tokens", baseline: baseline.avgTokens, canary: canary.avgTokens, threshold: "+50%" });
|
|
3934
|
+
}
|
|
3935
|
+
if (canary.fallbackRate - baseline.fallbackRate >= 0.1) {
|
|
3936
|
+
triggers.push({ metric: "fallback", baseline: baseline.fallbackRate, canary: canary.fallbackRate, threshold: "+10pp" });
|
|
3937
|
+
}
|
|
3938
|
+
if (canary.damageRate - baseline.damageRate >= 0.1) {
|
|
3939
|
+
triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
|
|
3940
|
+
}
|
|
3941
|
+
const canarySampleAdequacy = Math.min(1, canary.appliedRuns / minCanaryRuns);
|
|
3942
|
+
const baselineSampleAdequacy = Math.min(1, baseline.appliedRuns / Math.max(20, minCanaryRuns));
|
|
3943
|
+
const dataConfidence = Math.round(100 * (canarySampleAdequacy * 0.25 + baselineSampleAdequacy * 0.15 + canary.qualityCoverage * canarySampleAdequacy * 0.2 + canary.damageCoverage * canarySampleAdequacy * 0.2 + baseline.damageCoverage * baselineSampleAdequacy * 0.2));
|
|
3944
|
+
const reasons = [];
|
|
3945
|
+
let decision = "hold";
|
|
3946
|
+
if (triggers.length && canary.appliedRuns >= 3) {
|
|
3947
|
+
decision = "rollback";
|
|
3948
|
+
reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
|
|
3949
|
+
} else if (canary.appliedRuns < minCanaryRuns) {
|
|
3950
|
+
reasons.push("need " + (minCanaryRuns - canary.appliedRuns) + " more canary runs with applied outcomes");
|
|
3951
|
+
} else if (baseline.appliedRuns < Math.max(20, minCanaryRuns)) {
|
|
3952
|
+
reasons.push("stable baseline is too small");
|
|
3953
|
+
} else if (canary.qualityCoverage < 0.7) {
|
|
3954
|
+
reasons.push("schema-v2 quality coverage is below 70%");
|
|
3955
|
+
} else if (canary.damageCoverage < 0.7) {
|
|
3956
|
+
reasons.push("correlated canary damage-observation coverage is below 70%");
|
|
3957
|
+
} else if (baseline.damageCoverage < 0.7) {
|
|
3958
|
+
reasons.push("correlated stable damage-observation coverage is below 70%");
|
|
3959
|
+
} else if ((canary.avgQuality ?? 0) < 85) {
|
|
3960
|
+
reasons.push("absolute verifier quality is below 85");
|
|
3961
|
+
} else if (canary.successRate < 0.949999) {
|
|
3962
|
+
reasons.push("absolute success rate is below 95%");
|
|
3963
|
+
} else {
|
|
3964
|
+
decision = "promote";
|
|
3965
|
+
reasons.push("sample, absolute quality, reliability, latency, token, fallback, and damage gates passed");
|
|
3966
|
+
}
|
|
3967
|
+
return {
|
|
3968
|
+
version: options.version,
|
|
3969
|
+
decision,
|
|
3970
|
+
dataConfidence,
|
|
3971
|
+
baseline: roundStats(baseline),
|
|
3972
|
+
canary: roundStats(canary),
|
|
3973
|
+
triggers,
|
|
3974
|
+
reasons
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
var TELEMETRY_FAILURE_KINDS = new Set([
|
|
3978
|
+
"cancelled",
|
|
3979
|
+
"timeout",
|
|
3980
|
+
"rate-limit",
|
|
3981
|
+
"authentication",
|
|
3982
|
+
"budget",
|
|
3983
|
+
"output-limit",
|
|
3984
|
+
"provider",
|
|
3985
|
+
"persistence",
|
|
3986
|
+
"validation",
|
|
3987
|
+
"verification",
|
|
3988
|
+
"yield",
|
|
3989
|
+
"internal"
|
|
3990
|
+
]);
|
|
3991
|
+
function isTelemetryFailureKind(value) {
|
|
3992
|
+
return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3712
3995
|
// src/utils/cache.ts
|
|
3713
3996
|
var INTERNAL_PHASES = new Set([
|
|
3714
3997
|
"explore-retry",
|
|
@@ -3822,7 +4105,8 @@ async function trackedComplete(phase, model, reqBody, opts, services) {
|
|
|
3822
4105
|
cacheHitTokens: 0,
|
|
3823
4106
|
cacheWriteTokens: 0,
|
|
3824
4107
|
latencyMs: Date.now() - start,
|
|
3825
|
-
success: false
|
|
4108
|
+
success: false,
|
|
4109
|
+
failureKind: classifyTelemetryFailure(err)
|
|
3826
4110
|
}, svc);
|
|
3827
4111
|
throw err;
|
|
3828
4112
|
}
|
|
@@ -4430,11 +4714,17 @@ function batchOutputLimit(mode, chunks, providerMax) {
|
|
|
4430
4714
|
const budget = MODE_POLICIES[mode].batchOutput;
|
|
4431
4715
|
return Math.min(Math.max(budget.min, chunks * budget.perChunk), budget.max, providerMax);
|
|
4432
4716
|
}
|
|
4433
|
-
function effectiveBudget(configured, modeDefault) {
|
|
4717
|
+
function effectiveBudget(configured, modeDefault, override) {
|
|
4718
|
+
if (override !== undefined && override > 0)
|
|
4719
|
+
return override;
|
|
4434
4720
|
if (configured <= 0)
|
|
4435
4721
|
return modeDefault;
|
|
4436
4722
|
return Math.min(configured, modeDefault);
|
|
4437
4723
|
}
|
|
4724
|
+
function resolveCallBudget(configured, mode, override, automatic = false) {
|
|
4725
|
+
const budget = effectiveBudget(configured, MODE_POLICIES[mode].maxLlmCalls, override);
|
|
4726
|
+
return automatic ? Math.min(budget, AUTO_TRIGGER_MAX_LLM_CALLS) : budget;
|
|
4727
|
+
}
|
|
4438
4728
|
|
|
4439
4729
|
// src/ui/overlays.ts
|
|
4440
4730
|
import { DynamicBorder as DynamicBorder2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -4913,6 +5203,27 @@ function readRemediationHints(projectId) {
|
|
|
4913
5203
|
return data.files.filter((f) => typeof f === "string");
|
|
4914
5204
|
}
|
|
4915
5205
|
|
|
5206
|
+
// src/infra/ai-messages.ts
|
|
5207
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
5208
|
+
import { convertToLlm, sessionEntryToContextMessages, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
5209
|
+
function contextMessageEntries(entries) {
|
|
5210
|
+
return entries.flatMap((entry) => convertToLlm(sessionEntryToContextMessages(entry)).map((message) => ({ type: "message", id: entry.id, message })));
|
|
5211
|
+
}
|
|
5212
|
+
function serializeConversationText(messages) {
|
|
5213
|
+
return asSerializableMessages(messages).map((message) => message.role === "toolResult" ? "[Tool result]: " + contentText(message.content, "") : serializeConversation([message])).filter(Boolean).join(`
|
|
5214
|
+
|
|
5215
|
+
`);
|
|
5216
|
+
}
|
|
5217
|
+
function asBranchMessage(message) {
|
|
5218
|
+
return message;
|
|
5219
|
+
}
|
|
5220
|
+
function asSerializableMessages(msgs) {
|
|
5221
|
+
return msgs;
|
|
5222
|
+
}
|
|
5223
|
+
function scrubLlmMessages(msgs, scrubber) {
|
|
5224
|
+
return scrubber.scrubValue(msgs).value;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
4916
5227
|
// src/app/run-context.ts
|
|
4917
5228
|
function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
|
|
4918
5229
|
rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
|
|
@@ -4947,12 +5258,9 @@ function advance(rc, stage) {
|
|
|
4947
5258
|
const marker = stage;
|
|
4948
5259
|
const index = STAGE_ORDER.indexOf(marker);
|
|
4949
5260
|
const record = rc;
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
if (record[STAGE_ORDER[prior]] !== true) {
|
|
4954
|
-
throw new Error("Pipeline stage out of order: " + marker + " requires " + STAGE_ORDER[prior]);
|
|
4955
|
-
}
|
|
5261
|
+
for (let prior = 0;prior < index; prior++) {
|
|
5262
|
+
if (record[STAGE_ORDER[prior]] !== true) {
|
|
5263
|
+
throw new Error("Pipeline stage out of order: " + marker + " requires " + STAGE_ORDER[prior]);
|
|
4956
5264
|
}
|
|
4957
5265
|
}
|
|
4958
5266
|
for (const field of STAGE_REQUIRED_FIELDS[marker]) {
|
|
@@ -5138,7 +5446,7 @@ function resolveCompactionWindow(rc) {
|
|
|
5138
5446
|
const totalTokens = rc.ctx.getContextUsage()?.tokens ?? 0;
|
|
5139
5447
|
const manager = rc.ctx.sessionManager;
|
|
5140
5448
|
const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
|
|
5141
|
-
const msgs = branch
|
|
5449
|
+
const msgs = contextMessageEntries(branch);
|
|
5142
5450
|
if (msgs.length < 3) {
|
|
5143
5451
|
if (rc.flags.force)
|
|
5144
5452
|
rc.notify("Manual compaction skipped: fewer than 3 active messages are available.", "warning");
|
|
@@ -5224,12 +5532,12 @@ function preparePreflightProfile(input) {
|
|
|
5224
5532
|
}
|
|
5225
5533
|
function prepareManualPreflightContext(ctx, summaryModel, tokenCalibration) {
|
|
5226
5534
|
const branch = typeof ctx.sessionManager.buildContextEntries === "function" ? ctx.sessionManager.buildContextEntries() : ctx.sessionManager.getBranch();
|
|
5227
|
-
const msgs = branch
|
|
5535
|
+
const msgs = contextMessageEntries(branch);
|
|
5228
5536
|
const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
|
|
5229
5537
|
const modelContextWindow = ctx.model?.contextWindow;
|
|
5230
5538
|
const contextWindowTokens = Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 ? modelContextWindow : 0;
|
|
5231
5539
|
const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
|
|
5232
|
-
const toolPercent = computeToolCharPercentage(
|
|
5540
|
+
const toolPercent = computeToolCharPercentage(msgs);
|
|
5233
5541
|
const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
|
|
5234
5542
|
const estimator = makeTokenEstimator(summaryModel.provider, summaryModel.id, tokenCalibration);
|
|
5235
5543
|
const messageTokens = msgs.map((entry) => estimator.message(entry.message));
|
|
@@ -6118,7 +6426,7 @@ function formatPreflightSummary(preflight, modelLabel, details = false) {
|
|
|
6118
6426
|
lines2.push("Estimator messages ~" + tokenCount(preflight.rawEstimatedMessageTokens) + " \xB7 normalization unavailable", "Route " + modelLabel + " \xB7 viability " + preflight.reason);
|
|
6119
6427
|
return lines2;
|
|
6120
6428
|
}
|
|
6121
|
-
const stateReserve = Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO);
|
|
6429
|
+
const stateReserve = Math.max(0, (plan.finalSummaryAllowanceTokens ?? plan.summaryBudgetTokens + Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO)) - plan.summaryBudgetTokens);
|
|
6122
6430
|
const lines = [
|
|
6123
6431
|
"Plan " + compactTokenCount(preflight.totalTokens) + " \u2192 ~" + compactTokenCount(plan.projectedAfterTokens) + " \xB7 ~" + compactTokenCount(plan.projectedSavedTokens) + " saved (" + percent(plan.projectedYield * 100) + ")",
|
|
6124
6432
|
"Keep ~" + compactTokenCount(plan.retainedTokens) + " recent \xB7 summary up to " + compactTokenCount(plan.summaryBudgetTokens) + " + ~" + compactTokenCount(stateReserve) + " verified-state reserve",
|
|
@@ -6298,12 +6606,15 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
|
|
|
6298
6606
|
c.addChild(new Text2(theme.fg("text", opts.summary), 2, 0));
|
|
6299
6607
|
c.addChild(new Text2("", 0, 0));
|
|
6300
6608
|
}
|
|
6609
|
+
const scrollbarStyle = (text) => theme.fg("borderMuted", text);
|
|
6301
6610
|
const scroll = new ScrollView(c, {
|
|
6302
6611
|
follow: "none",
|
|
6303
6612
|
primary: true,
|
|
6304
6613
|
overscroll: "contain",
|
|
6305
6614
|
scrollbar: "auto",
|
|
6306
|
-
scrollbarStyle
|
|
6615
|
+
scrollbarStyle,
|
|
6616
|
+
scrollbarTrackStyle: scrollbarStyle,
|
|
6617
|
+
scrollbarThumbStyle: scrollbarStyle
|
|
6307
6618
|
});
|
|
6308
6619
|
const footer = new Container2;
|
|
6309
6620
|
footer.addChild(new DynamicBorder2((s) => theme.fg("accent", s)));
|
|
@@ -6405,8 +6716,8 @@ async function showCompactUI(ctx, opts) {
|
|
|
6405
6716
|
const marker = index === selected ? "\u203A " : " ";
|
|
6406
6717
|
const recommendedMark = mode === recommended.mode ? " recommended" : " ";
|
|
6407
6718
|
const trait = mode === "fast" ? "quickest" : mode === "balanced" ? "default" : "deepest";
|
|
6408
|
-
const
|
|
6409
|
-
const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " +
|
|
6719
|
+
const stats2 = (viable && plan ? "~" + compactTokenCount(plan.projectedAfterTokens) + " after \xB7 " + percent(plan.projectedYield * 100) + " saved" : "unavailable \xB7 " + explainPreflightReason(preview.reason)) + " \xB7 " + trait;
|
|
6720
|
+
const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " + stats2;
|
|
6410
6721
|
lines.push(cell(index === selected ? theme.fg("accent", theme.bold(line)) : theme.fg(viable ? mode === recommended.mode ? "success" : "text" : "muted", line)));
|
|
6411
6722
|
}
|
|
6412
6723
|
lines.push(divider);
|
|
@@ -6499,8 +6810,8 @@ async function prepareRun(rc) {
|
|
|
6499
6810
|
rc.timeoutMs = rc.timeoutMs > 0 ? Math.min(rc.timeoutMs, config.maxLatencyMs) : config.maxLatencyMs;
|
|
6500
6811
|
}
|
|
6501
6812
|
const policy = MODE_POLICIES[rc.mode];
|
|
6502
|
-
const callBudget =
|
|
6503
|
-
const inputBudget =
|
|
6813
|
+
const callBudget = resolveCallBudget(config.maxLlmCalls, rc.mode, rc.maxLlmCalls, rc.flags.autoTriggered && !rc.flags.skipCompact);
|
|
6814
|
+
const inputBudget = effectiveBudget(config.maxLlmInputTokens, policy.maxInputTokens, rc.maxLlmInputTokens);
|
|
6504
6815
|
rc.services.budget = new BudgetGuard(callBudget, rc.timeoutMs, rc.services.clock, inputBudget, policy.maxOutputTokens);
|
|
6505
6816
|
if (rc.timeoutMs > 0) {
|
|
6506
6817
|
rc.cancellation.timeoutId = setTimeout(() => {
|
|
@@ -6520,24 +6831,13 @@ async function prepareRun(rc) {
|
|
|
6520
6831
|
}
|
|
6521
6832
|
|
|
6522
6833
|
// src/app/steps/recover.ts
|
|
6523
|
-
import { convertToLlm as
|
|
6524
|
-
|
|
6525
|
-
// src/infra/ai-messages.ts
|
|
6526
|
-
function asBranchMessage(message) {
|
|
6527
|
-
return message;
|
|
6528
|
-
}
|
|
6529
|
-
function asSerializableMessages(msgs) {
|
|
6530
|
-
return msgs;
|
|
6531
|
-
}
|
|
6532
|
-
function scrubLlmMessages(msgs, scrubber) {
|
|
6533
|
-
return scrubber.scrubValue(msgs).value;
|
|
6534
|
-
}
|
|
6834
|
+
import { convertToLlm as convertToLlm3 } from "@earendil-works/pi-coding-agent";
|
|
6535
6835
|
|
|
6536
6836
|
// src/utils/session-log.ts
|
|
6537
6837
|
import * as fs6 from "fs";
|
|
6538
6838
|
import * as path10 from "path";
|
|
6539
6839
|
import { StringDecoder } from "string_decoder";
|
|
6540
|
-
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
6840
|
+
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
6541
6841
|
function getSessionsDir() {
|
|
6542
6842
|
return sessionsDir();
|
|
6543
6843
|
}
|
|
@@ -6672,10 +6972,10 @@ async function readOriginalMessageMap(sessionId, wantedIds, cwd) {
|
|
|
6672
6972
|
} catch {
|
|
6673
6973
|
continue;
|
|
6674
6974
|
}
|
|
6675
|
-
if (
|
|
6975
|
+
if (!entry.id || !remaining.has(entry.id))
|
|
6676
6976
|
continue;
|
|
6677
6977
|
remaining.delete(entry.id);
|
|
6678
|
-
const normalized = normalizeLogMessage(entry.message, entry.timestamp);
|
|
6978
|
+
const normalized = entry.type === "message" && entry.message ? normalizeLogMessage(entry.message, entry.timestamp) : contextMessageEntries([entry])[0]?.message;
|
|
6679
6979
|
if (normalized)
|
|
6680
6980
|
map.set(entry.id, normalized);
|
|
6681
6981
|
if (remaining.size === 0)
|
|
@@ -6705,7 +7005,7 @@ async function resolveCompactionMessages(sessionId, toCompactEntries, cwd) {
|
|
|
6705
7005
|
for (const entry of toCompactEntries) {
|
|
6706
7006
|
if (!entry.id)
|
|
6707
7007
|
continue;
|
|
6708
|
-
const converted =
|
|
7008
|
+
const converted = convertToLlm2([
|
|
6709
7009
|
asBranchMessage(entry.message)
|
|
6710
7010
|
]);
|
|
6711
7011
|
if (!converted.length)
|
|
@@ -6730,7 +7030,7 @@ async function recoverSessionLog(rc) {
|
|
|
6730
7030
|
let resolved = rc.toCompact.flatMap((entry) => {
|
|
6731
7031
|
if (!entry.id)
|
|
6732
7032
|
return [];
|
|
6733
|
-
return
|
|
7033
|
+
return convertToLlm3([asBranchMessage(entry.message)]).map((message) => ({ entryId: entry.id, message }));
|
|
6734
7034
|
});
|
|
6735
7035
|
if (hasTruncatedMessages(resolved.map((item) => item.message))) {
|
|
6736
7036
|
const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact, rc.ctx.cwd);
|
|
@@ -6747,7 +7047,7 @@ async function recoverSessionLog(rc) {
|
|
|
6747
7047
|
|
|
6748
7048
|
// src/app/steps/tier.ts
|
|
6749
7049
|
function selectTier(rc) {
|
|
6750
|
-
const toolPercent = computeToolCharPercentage(rc.
|
|
7050
|
+
const toolPercent = computeToolCharPercentage(rc.msgs);
|
|
6751
7051
|
const tier = rc.flags.overflowRecovery ? "full" : rc.flags.force ? rc.contextPercent >= 80 ? "full" : "light" : selectCompactionTier(rc.contextPercent, rc.totalTokens, MIN_TOKEN_THRESHOLD, rc.config.minContextPercent);
|
|
6752
7052
|
if (tier === "none") {
|
|
6753
7053
|
if (!rc.flags.autoTriggered) {
|
|
@@ -6914,9 +7214,6 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
6914
7214
|
};
|
|
6915
7215
|
}
|
|
6916
7216
|
|
|
6917
|
-
// src/app/steps/extract.ts
|
|
6918
|
-
import { serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
6919
|
-
|
|
6920
7217
|
// src/utils/backups.ts
|
|
6921
7218
|
import fs7 from "fs";
|
|
6922
7219
|
import path11 from "path";
|
|
@@ -7142,7 +7439,7 @@ function extractWithCache(rc) {
|
|
|
7142
7439
|
const pruneEnd = Date.now();
|
|
7143
7440
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
7144
7441
|
const extractionStart = pruneEnd;
|
|
7145
|
-
const convText = rc.services.scrubber.scrubText(
|
|
7442
|
+
const convText = rc.services.scrubber.scrubText(serializeConversationText(rc.llmMessages)).value;
|
|
7146
7443
|
const convTokens = rc.estimator.text(convText);
|
|
7147
7444
|
let preparedBackup;
|
|
7148
7445
|
if (rc.config.backupEnabled) {
|
|
@@ -7150,7 +7447,7 @@ function extractWithCache(rc) {
|
|
|
7150
7447
|
if (pruningUnchanged)
|
|
7151
7448
|
return convText;
|
|
7152
7449
|
const safeMessages = scrubLlmMessages(selectedMessages, rc.services.scrubber);
|
|
7153
|
-
const backupText =
|
|
7450
|
+
const backupText = serializeConversationText(safeMessages);
|
|
7154
7451
|
const scrubbed = rc.services.scrubber.scrubText(backupText);
|
|
7155
7452
|
if (scrubbed.findings.length > 0) {
|
|
7156
7453
|
rc.notify("Backup written with redactions (" + scrubbed.findings.map((f) => f.count + "x " + f.kind).join(", ") + ") \u2014 restore will lack that data", "info");
|
|
@@ -7257,187 +7554,6 @@ function extractWithCache(rc) {
|
|
|
7257
7554
|
|
|
7258
7555
|
// src/phases/explore.ts
|
|
7259
7556
|
import { Type } from "typebox";
|
|
7260
|
-
|
|
7261
|
-
// src/domain/telemetry.ts
|
|
7262
|
-
function errorFields(error2) {
|
|
7263
|
-
if (!error2 || typeof error2 !== "object") {
|
|
7264
|
-
return { name: "", message: String(error2 ?? ""), status: null, code: "" };
|
|
7265
|
-
}
|
|
7266
|
-
const value = error2;
|
|
7267
|
-
const cause = value.cause && value.cause !== error2 ? errorFields(value.cause) : null;
|
|
7268
|
-
const numericStatus = Number(value.status ?? value.statusCode);
|
|
7269
|
-
return {
|
|
7270
|
-
name: typeof value.name === "string" ? value.name : cause?.name ?? "",
|
|
7271
|
-
message: (typeof value.message === "string" ? value.message : "") + (cause?.message ? " " + cause.message : ""),
|
|
7272
|
-
status: Number.isFinite(numericStatus) ? numericStatus : cause?.status ?? null,
|
|
7273
|
-
code: typeof value.code === "string" ? value.code : cause?.code ?? ""
|
|
7274
|
-
};
|
|
7275
|
-
}
|
|
7276
|
-
function classifyTelemetryFailure(error2, timedOut = false) {
|
|
7277
|
-
const fields = errorFields(error2);
|
|
7278
|
-
const text = (fields.name + " " + fields.code + " " + fields.message).toLowerCase();
|
|
7279
|
-
if (timedOut || /timeout|timed out|watchdog|deadline/.test(text))
|
|
7280
|
-
return "timeout";
|
|
7281
|
-
if (fields.name.toLowerCase() === "verificationgateerror")
|
|
7282
|
-
return "verification";
|
|
7283
|
-
if (fields.name.toLowerCase() === "yieldgateerror")
|
|
7284
|
-
return "yield";
|
|
7285
|
-
if (/budgetexceeded|token budget|call budget|latency budget/.test(text))
|
|
7286
|
-
return "budget";
|
|
7287
|
-
if (fields.status === 429 || /rate.?limit|too many requests|quota/.test(text))
|
|
7288
|
-
return "rate-limit";
|
|
7289
|
-
if (fields.status === 401 || fields.status === 403 || /unauthori[sz]ed|authentication|api.?key|credential/.test(text))
|
|
7290
|
-
return "authentication";
|
|
7291
|
-
if (/max(?:imum)? output|output.?limit|visible output|length limit/.test(text))
|
|
7292
|
-
return "output-limit";
|
|
7293
|
-
if (/abort|cancel/.test(text))
|
|
7294
|
-
return "cancelled";
|
|
7295
|
-
if (/native compaction|persist|write|rename|filesystem|sqlite|database/.test(text))
|
|
7296
|
-
return "persistence";
|
|
7297
|
-
if (/verificationgateerror|verification gate|verification.*(?:gap|summary)/.test(text))
|
|
7298
|
-
return "verification";
|
|
7299
|
-
if (/invalid|validation|schema|malformed|required/.test(text))
|
|
7300
|
-
return "validation";
|
|
7301
|
-
if (fields.status != null && fields.status >= 500 || /provider|api error|stream|network|fetch failed|socket/.test(text))
|
|
7302
|
-
return "provider";
|
|
7303
|
-
return "internal";
|
|
7304
|
-
}
|
|
7305
|
-
function p95(values) {
|
|
7306
|
-
if (!values.length)
|
|
7307
|
-
return 0;
|
|
7308
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
7309
|
-
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
|
|
7310
|
-
}
|
|
7311
|
-
function stats(entries, damage) {
|
|
7312
|
-
const evidence = entries.filter((entry) => entry.status !== "dry-run");
|
|
7313
|
-
const successfulRuns = evidence.filter((entry) => entry.status === "success");
|
|
7314
|
-
const quality = successfulRuns.filter((entry) => typeof entry.verificationScore === "number");
|
|
7315
|
-
const appliedRunIds = new Set(successfulRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
|
|
7316
|
-
const observedScores = new Map;
|
|
7317
|
-
for (const observation of damage) {
|
|
7318
|
-
if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
|
|
7319
|
-
continue;
|
|
7320
|
-
observedScores.set(observation.runId, Math.max(observedScores.get(observation.runId) ?? 0, Math.max(0, Math.min(100, observation.damageScore))));
|
|
7321
|
-
}
|
|
7322
|
-
const damaging = [...observedScores.values()].filter((score) => score > 0).length;
|
|
7323
|
-
return {
|
|
7324
|
-
runs: entries.length,
|
|
7325
|
-
appliedRuns: evidence.length,
|
|
7326
|
-
successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
|
|
7327
|
-
avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
|
|
7328
|
-
qualityCoverage: evidence.length ? quality.length / evidence.length : 0,
|
|
7329
|
-
p95LatencyMs: p95(evidence.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
|
|
7330
|
-
avgTokens: evidence.length ? evidence.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / evidence.length : 0,
|
|
7331
|
-
fallbackRate: evidence.length ? evidence.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / evidence.length : 0,
|
|
7332
|
-
damageRate: observedScores.size ? damaging / observedScores.size : 0,
|
|
7333
|
-
damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
|
|
7334
|
-
};
|
|
7335
|
-
}
|
|
7336
|
-
function roundStats(value) {
|
|
7337
|
-
return {
|
|
7338
|
-
...value,
|
|
7339
|
-
successRate: Math.round(value.successRate * 1000) / 1000,
|
|
7340
|
-
avgQuality: value.avgQuality == null ? null : Math.round(value.avgQuality * 10) / 10,
|
|
7341
|
-
qualityCoverage: Math.round(value.qualityCoverage * 1000) / 1000,
|
|
7342
|
-
p95LatencyMs: Math.round(value.p95LatencyMs),
|
|
7343
|
-
avgTokens: Math.round(value.avgTokens),
|
|
7344
|
-
fallbackRate: Math.round(value.fallbackRate * 1000) / 1000,
|
|
7345
|
-
damageRate: Math.round(value.damageRate * 1000) / 1000,
|
|
7346
|
-
damageCoverage: Math.round(value.damageCoverage * 1000) / 1000
|
|
7347
|
-
};
|
|
7348
|
-
}
|
|
7349
|
-
function assessCanary(entries, damageEntries, options) {
|
|
7350
|
-
const minCanaryRuns = Math.max(5, options.minCanaryRuns ?? 20);
|
|
7351
|
-
const canaryEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && entry.version === options.version && entry.releaseChannel === "canary").slice(-Math.max(100, minCanaryRuns));
|
|
7352
|
-
const baselineEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && (entry.releaseChannel ?? "stable") === "stable").slice(-(options.baselineRuns ?? Math.max(50, minCanaryRuns * 2)));
|
|
7353
|
-
const baseline = stats(baselineEntries, damageEntries);
|
|
7354
|
-
const canary = stats(canaryEntries, damageEntries);
|
|
7355
|
-
const triggers = [];
|
|
7356
|
-
const failureBaseline = 1 - baseline.successRate;
|
|
7357
|
-
const failureCanary = 1 - canary.successRate;
|
|
7358
|
-
if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
|
|
7359
|
-
triggers.push({
|
|
7360
|
-
metric: "failure-rate",
|
|
7361
|
-
baseline: failureBaseline,
|
|
7362
|
-
canary: failureCanary,
|
|
7363
|
-
threshold: failureCanary > 0.050001 ? ">5% absolute" : "+5pp regression"
|
|
7364
|
-
});
|
|
7365
|
-
}
|
|
7366
|
-
if (canary.avgQuality != null && (canary.avgQuality < 85 || baseline.avgQuality != null && baseline.avgQuality - canary.avgQuality >= 5)) {
|
|
7367
|
-
triggers.push({
|
|
7368
|
-
metric: "quality",
|
|
7369
|
-
baseline: baseline.avgQuality ?? 0,
|
|
7370
|
-
canary: canary.avgQuality,
|
|
7371
|
-
threshold: canary.avgQuality < 85 ? "<85 absolute" : "-5 points"
|
|
7372
|
-
});
|
|
7373
|
-
}
|
|
7374
|
-
if (baseline.p95LatencyMs >= 1000 && canary.p95LatencyMs >= baseline.p95LatencyMs * 1.5) {
|
|
7375
|
-
triggers.push({ metric: "latency", baseline: baseline.p95LatencyMs, canary: canary.p95LatencyMs, threshold: "+50% p95" });
|
|
7376
|
-
}
|
|
7377
|
-
if (baseline.avgTokens >= 1000 && canary.avgTokens >= baseline.avgTokens * 1.5) {
|
|
7378
|
-
triggers.push({ metric: "tokens", baseline: baseline.avgTokens, canary: canary.avgTokens, threshold: "+50%" });
|
|
7379
|
-
}
|
|
7380
|
-
if (canary.fallbackRate - baseline.fallbackRate >= 0.1) {
|
|
7381
|
-
triggers.push({ metric: "fallback", baseline: baseline.fallbackRate, canary: canary.fallbackRate, threshold: "+10pp" });
|
|
7382
|
-
}
|
|
7383
|
-
if (canary.damageRate - baseline.damageRate >= 0.1) {
|
|
7384
|
-
triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
|
|
7385
|
-
}
|
|
7386
|
-
const canarySampleAdequacy = Math.min(1, canary.appliedRuns / minCanaryRuns);
|
|
7387
|
-
const baselineSampleAdequacy = Math.min(1, baseline.appliedRuns / Math.max(20, minCanaryRuns));
|
|
7388
|
-
const dataConfidence = Math.round(100 * (canarySampleAdequacy * 0.25 + baselineSampleAdequacy * 0.15 + canary.qualityCoverage * canarySampleAdequacy * 0.2 + canary.damageCoverage * canarySampleAdequacy * 0.2 + baseline.damageCoverage * baselineSampleAdequacy * 0.2));
|
|
7389
|
-
const reasons = [];
|
|
7390
|
-
let decision = "hold";
|
|
7391
|
-
if (triggers.length && canary.appliedRuns >= 3) {
|
|
7392
|
-
decision = "rollback";
|
|
7393
|
-
reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
|
|
7394
|
-
} else if (canary.appliedRuns < minCanaryRuns) {
|
|
7395
|
-
reasons.push("need " + (minCanaryRuns - canary.appliedRuns) + " more canary runs with applied outcomes");
|
|
7396
|
-
} else if (baseline.appliedRuns < Math.max(20, minCanaryRuns)) {
|
|
7397
|
-
reasons.push("stable baseline is too small");
|
|
7398
|
-
} else if (canary.qualityCoverage < 0.7) {
|
|
7399
|
-
reasons.push("schema-v2 quality coverage is below 70%");
|
|
7400
|
-
} else if (canary.damageCoverage < 0.7) {
|
|
7401
|
-
reasons.push("correlated canary damage-observation coverage is below 70%");
|
|
7402
|
-
} else if (baseline.damageCoverage < 0.7) {
|
|
7403
|
-
reasons.push("correlated stable damage-observation coverage is below 70%");
|
|
7404
|
-
} else if ((canary.avgQuality ?? 0) < 85) {
|
|
7405
|
-
reasons.push("absolute verifier quality is below 85");
|
|
7406
|
-
} else if (canary.successRate < 0.949999) {
|
|
7407
|
-
reasons.push("absolute success rate is below 95%");
|
|
7408
|
-
} else {
|
|
7409
|
-
decision = "promote";
|
|
7410
|
-
reasons.push("sample, absolute quality, reliability, latency, token, fallback, and damage gates passed");
|
|
7411
|
-
}
|
|
7412
|
-
return {
|
|
7413
|
-
version: options.version,
|
|
7414
|
-
decision,
|
|
7415
|
-
dataConfidence,
|
|
7416
|
-
baseline: roundStats(baseline),
|
|
7417
|
-
canary: roundStats(canary),
|
|
7418
|
-
triggers,
|
|
7419
|
-
reasons
|
|
7420
|
-
};
|
|
7421
|
-
}
|
|
7422
|
-
var TELEMETRY_FAILURE_KINDS = new Set([
|
|
7423
|
-
"cancelled",
|
|
7424
|
-
"timeout",
|
|
7425
|
-
"rate-limit",
|
|
7426
|
-
"authentication",
|
|
7427
|
-
"budget",
|
|
7428
|
-
"output-limit",
|
|
7429
|
-
"provider",
|
|
7430
|
-
"persistence",
|
|
7431
|
-
"validation",
|
|
7432
|
-
"verification",
|
|
7433
|
-
"yield",
|
|
7434
|
-
"internal"
|
|
7435
|
-
]);
|
|
7436
|
-
function isTelemetryFailureKind(value) {
|
|
7437
|
-
return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
|
|
7438
|
-
}
|
|
7439
|
-
|
|
7440
|
-
// src/phases/explore.ts
|
|
7441
7557
|
function explicitlyRejectsTools(error2) {
|
|
7442
7558
|
if (!error2 || typeof error2 !== "object")
|
|
7443
7559
|
return false;
|
|
@@ -7501,8 +7617,12 @@ function boundedExplorationValue(value, depth = 0) {
|
|
|
7501
7617
|
if (typeof value === "string") {
|
|
7502
7618
|
return value.length > TRUNC.PREVIEW_XL ? value.slice(0, TRUNC.PREVIEW_XL) + "\u2026" : value;
|
|
7503
7619
|
}
|
|
7504
|
-
if (value == null || typeof value
|
|
7620
|
+
if (value == null || typeof value === "number" || typeof value === "boolean")
|
|
7505
7621
|
return value;
|
|
7622
|
+
if (typeof value === "bigint")
|
|
7623
|
+
return value.toString();
|
|
7624
|
+
if (typeof value !== "object")
|
|
7625
|
+
return;
|
|
7506
7626
|
if (depth >= 3)
|
|
7507
7627
|
return "[bounded]";
|
|
7508
7628
|
if (Array.isArray(value))
|
|
@@ -7511,7 +7631,7 @@ function boundedExplorationValue(value, depth = 0) {
|
|
|
7511
7631
|
}
|
|
7512
7632
|
function serializeExplorationResult(value, scrubber) {
|
|
7513
7633
|
const safe = boundedExplorationValue(scrubber.scrubValue(value).value);
|
|
7514
|
-
const serialized = JSON.stringify(safe);
|
|
7634
|
+
const serialized = JSON.stringify(safe) ?? "null";
|
|
7515
7635
|
if (serialized.length <= MAX_EXPLORER_OUTPUT_CHARS)
|
|
7516
7636
|
return serialized;
|
|
7517
7637
|
let excerptChars = Math.max(0, Math.floor((MAX_EXPLORER_OUTPUT_CHARS - 160) / 2));
|
|
@@ -8160,16 +8280,75 @@ function batchFieldPattern(name) {
|
|
|
8160
8280
|
let pattern = batchFieldPatterns.get(name);
|
|
8161
8281
|
if (!pattern) {
|
|
8162
8282
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8163
|
-
pattern = new RegExp("\\*\\*" + escaped + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i");
|
|
8283
|
+
pattern = new RegExp("(?:^|\\n)\\*\\*" + escaped + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i");
|
|
8164
8284
|
batchFieldPatterns.set(name, pattern);
|
|
8165
8285
|
}
|
|
8166
8286
|
return pattern;
|
|
8167
8287
|
}
|
|
8288
|
+
|
|
8289
|
+
class BatchSummaryFormatError extends Error {
|
|
8290
|
+
name = "BatchSummaryFormatError";
|
|
8291
|
+
constructor(reason) {
|
|
8292
|
+
super("Malformed batch summary response: " + reason);
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8295
|
+
var BATCH_REQUIRED_FIELDS = [
|
|
8296
|
+
"Priority",
|
|
8297
|
+
"Summary",
|
|
8298
|
+
"Decisions",
|
|
8299
|
+
"Modified",
|
|
8300
|
+
"Deleted",
|
|
8301
|
+
"Read"
|
|
8302
|
+
];
|
|
8303
|
+
function assertCompleteBatchResponse(stopReason, sectionMap, duplicateIds, batchSize) {
|
|
8304
|
+
const reason = String(stopReason ?? "");
|
|
8305
|
+
if (reason !== "stop" && reason !== "endTurn") {
|
|
8306
|
+
throw new BatchSummaryFormatError("non-terminal stop reason " + (reason || "unknown"));
|
|
8307
|
+
}
|
|
8308
|
+
if (duplicateIds.size > 0) {
|
|
8309
|
+
throw new BatchSummaryFormatError("duplicate chunk id(s): " + [...duplicateIds].sort((a, b) => a - b).join(", "));
|
|
8310
|
+
}
|
|
8311
|
+
const unexpected = [...sectionMap.keys()].filter((id) => id < 1 || id > batchSize);
|
|
8312
|
+
if (unexpected.length > 0) {
|
|
8313
|
+
throw new BatchSummaryFormatError("unexpected chunk id(s): " + unexpected.sort((a, b) => a - b).join(", "));
|
|
8314
|
+
}
|
|
8315
|
+
const missingSections = [];
|
|
8316
|
+
for (let id = 1;id <= batchSize; id++) {
|
|
8317
|
+
if (!sectionMap.has(id))
|
|
8318
|
+
missingSections.push(id);
|
|
8319
|
+
}
|
|
8320
|
+
if (missingSections.length > 0) {
|
|
8321
|
+
throw new BatchSummaryFormatError("missing chunk section(s): " + missingSections.join(", "));
|
|
8322
|
+
}
|
|
8323
|
+
for (let id = 1;id <= batchSize; id++) {
|
|
8324
|
+
const section = sectionMap.get(id) ?? "";
|
|
8325
|
+
const missingFields = BATCH_REQUIRED_FIELDS.filter((field) => !batchFieldPattern(field).test(section));
|
|
8326
|
+
if (missingFields.length > 0) {
|
|
8327
|
+
throw new BatchSummaryFormatError("chunk " + id + " missing field(s): " + missingFields.join(", "));
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
const missing = [];
|
|
8331
|
+
for (let id = 1;id <= batchSize; id++) {
|
|
8332
|
+
const section = sectionMap.get(id);
|
|
8333
|
+
const summary = section?.match(batchFieldPattern("Summary"))?.[1].trim();
|
|
8334
|
+
if (!summary || summary.toLowerCase() === "none")
|
|
8335
|
+
missing.push(id);
|
|
8336
|
+
}
|
|
8337
|
+
if (missing.length > 0) {
|
|
8338
|
+
throw new BatchSummaryFormatError("missing usable Summary for chunk(s): " + missing.join(", "));
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8168
8341
|
function boundedToolArgs(value, depth = 0) {
|
|
8169
8342
|
if (typeof value === "string")
|
|
8170
8343
|
return value.length > TRUNC.DETAIL ? value.slice(0, TRUNC.DETAIL) + "\u2026" : value;
|
|
8171
|
-
if (value == null || typeof value
|
|
8344
|
+
if (value == null || typeof value === "number" || typeof value === "boolean")
|
|
8172
8345
|
return value;
|
|
8346
|
+
if (typeof value === "bigint")
|
|
8347
|
+
return value.toString();
|
|
8348
|
+
if (typeof value !== "object")
|
|
8349
|
+
return;
|
|
8350
|
+
if (depth >= 2)
|
|
8351
|
+
return "[bounded]";
|
|
8173
8352
|
if (Array.isArray(value))
|
|
8174
8353
|
return value.slice(0, 8).map((item) => boundedToolArgs(item, depth + 1));
|
|
8175
8354
|
return Object.fromEntries(Object.entries(value).slice(0, 12).map(([key, item]) => [key, boundedToolArgs(item, depth + 1)]));
|
|
@@ -8483,13 +8662,19 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
|
|
|
8483
8662
|
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
8484
8663
|
`);
|
|
8485
8664
|
const sectionMap = new Map;
|
|
8665
|
+
const duplicateIds = new Set;
|
|
8486
8666
|
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
8487
8667
|
for (const sec of sections) {
|
|
8488
8668
|
const m = sec.match(/^CHUNK\s+(\d+):\s*(.*?)\n/i);
|
|
8489
8669
|
if (m) {
|
|
8490
|
-
|
|
8670
|
+
const id = parseInt(m[1], 10);
|
|
8671
|
+
if (sectionMap.has(id))
|
|
8672
|
+
duplicateIds.add(id);
|
|
8673
|
+
else
|
|
8674
|
+
sectionMap.set(id, sec);
|
|
8491
8675
|
}
|
|
8492
8676
|
}
|
|
8677
|
+
assertCompleteBatchResponse(resp.stopReason, sectionMap, duplicateIds, batch.length);
|
|
8493
8678
|
const result = batch.map((ch, i) => {
|
|
8494
8679
|
const id = i + 1;
|
|
8495
8680
|
const sec = sectionMap.get(id) ?? "";
|
|
@@ -8687,377 +8872,21 @@ async function resolveStageAuth(rc, stage) {
|
|
|
8687
8872
|
return resolved;
|
|
8688
8873
|
}
|
|
8689
8874
|
|
|
8690
|
-
// src/
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
|
|
8703
|
-
}
|
|
8875
|
+
// src/phases/verify.ts
|
|
8876
|
+
var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
|
|
8877
|
+
var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
|
|
8878
|
+
var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
8879
|
+
var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
8880
|
+
var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
|
|
8881
|
+
function noneBlockerLineIndexes(lines) {
|
|
8882
|
+
const indexes = new Set;
|
|
8883
|
+
const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
|
|
8884
|
+
for (const item of nonEmpty) {
|
|
8885
|
+
if (BULLET_NONE_BLOCKER_RE.test(item.text))
|
|
8886
|
+
indexes.add(item.index);
|
|
8704
8887
|
}
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
const cacheKey = synthesisCacheKey(rc);
|
|
8708
|
-
const cached = getCachedSynthesis(cacheKey);
|
|
8709
|
-
if (cached) {
|
|
8710
|
-
rc.notify("Synthesis cache hit \u2014 no LLM calls", "info");
|
|
8711
|
-
showProgressOverlay(rc.ctx, {
|
|
8712
|
-
phase: 3,
|
|
8713
|
-
phaseName: "Synthesize",
|
|
8714
|
-
detail: "Reusing the cached continuation summary \xB7 no LLM call"
|
|
8715
|
-
});
|
|
8716
|
-
Object.assign(rc, {
|
|
8717
|
-
finalSummary: cached.finalSummary,
|
|
8718
|
-
method: cached.method,
|
|
8719
|
-
methodForMetrics: cached.method + "-cache",
|
|
8720
|
-
generationFallbacks: [],
|
|
8721
|
-
llmCalls: 0,
|
|
8722
|
-
summaries: cached.summaries,
|
|
8723
|
-
explorationReport: cached.explorationReport,
|
|
8724
|
-
explorationRounds: cached.explorationRounds,
|
|
8725
|
-
chunkCount: cached.chunkCount
|
|
8726
|
-
});
|
|
8727
|
-
const hit = advance(rc, "_synthesized");
|
|
8728
|
-
markMeasuredPhase(hit, "synthesize", synthPhaseStart);
|
|
8729
|
-
return hit;
|
|
8730
|
-
}
|
|
8731
|
-
const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
|
|
8732
|
-
conversationTokens: rc.convTokens,
|
|
8733
|
-
toolPercent: rc.toolPercent
|
|
8734
|
-
}) >= 0.85;
|
|
8735
|
-
if (zeroCall) {
|
|
8736
|
-
showProgressOverlay(rc.ctx, {
|
|
8737
|
-
phase: 3,
|
|
8738
|
-
phaseName: "Synthesize",
|
|
8739
|
-
detail: "Building a deterministic continuation summary \xB7 no LLM call"
|
|
8740
|
-
});
|
|
8741
|
-
const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
8742
|
-
setCachedSynthesis(cacheKey, {
|
|
8743
|
-
finalSummary: finalSummary2,
|
|
8744
|
-
method: "heuristic",
|
|
8745
|
-
summaries: [],
|
|
8746
|
-
explorationReport: null,
|
|
8747
|
-
explorationRounds: 0,
|
|
8748
|
-
chunkCount: 0
|
|
8749
|
-
});
|
|
8750
|
-
rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
|
|
8751
|
-
Object.assign(rc, {
|
|
8752
|
-
finalSummary: finalSummary2,
|
|
8753
|
-
method: "heuristic",
|
|
8754
|
-
methodForMetrics: "zero-call",
|
|
8755
|
-
generationFallbacks: [],
|
|
8756
|
-
llmCalls: 0,
|
|
8757
|
-
summaries: [],
|
|
8758
|
-
explorationReport: null,
|
|
8759
|
-
explorationRounds: 0,
|
|
8760
|
-
chunkCount: 0
|
|
8761
|
-
});
|
|
8762
|
-
const deterministic = advance(rc, "_synthesized");
|
|
8763
|
-
markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
|
|
8764
|
-
return deterministic;
|
|
8765
|
-
}
|
|
8766
|
-
const shouldSkipExplore = !policy.explore;
|
|
8767
|
-
const convText = rc.convText;
|
|
8768
|
-
const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * rc.providerCaps.singlePassTokenMultiplier * policy.singlePassMultiplier);
|
|
8769
|
-
rc.vlog("Tier=" + rc.tier + " | convTokens=" + rc.convTokens + " | singlePassMax=" + singlePassMaxTokens);
|
|
8770
|
-
let finalSummary;
|
|
8771
|
-
let method;
|
|
8772
|
-
const summaries = [];
|
|
8773
|
-
let explorationReport = null;
|
|
8774
|
-
let explorationRounds = 0;
|
|
8775
|
-
let chunkCount = 0;
|
|
8776
|
-
let cacheable = true;
|
|
8777
|
-
const generationFallbacks = [];
|
|
8778
|
-
let summaryAuth;
|
|
8779
|
-
try {
|
|
8780
|
-
summaryAuth = await resolveStageAuth(rc, "summary");
|
|
8781
|
-
} catch (error2) {
|
|
8782
|
-
cacheable = false;
|
|
8783
|
-
generationFallbacks.push("summary route unavailable");
|
|
8784
|
-
debugError("Summary route unavailable", error2);
|
|
8785
|
-
rc.notify("Summary route unavailable \xB7 using deterministic fallback", "info");
|
|
8786
|
-
}
|
|
8787
|
-
if (!summaryAuth) {
|
|
8788
|
-
showProgressOverlay(rc.ctx, {
|
|
8789
|
-
phase: 3,
|
|
8790
|
-
phaseName: "Synthesize",
|
|
8791
|
-
detail: "Summary route unavailable \xB7 building a deterministic summary"
|
|
8792
|
-
});
|
|
8793
|
-
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
8794
|
-
method = "heuristic";
|
|
8795
|
-
} else if (rc.convTokens < singlePassMaxTokens) {
|
|
8796
|
-
showProgressOverlay(rc.ctx, {
|
|
8797
|
-
phase: 3,
|
|
8798
|
-
phaseName: "Synthesize",
|
|
8799
|
-
detail: "Writing one continuation summary from " + rc.convTokens.toLocaleString() + " tokens",
|
|
8800
|
-
model: rc.modelLabel,
|
|
8801
|
-
profile: rc.profile,
|
|
8802
|
-
extraction
|
|
8803
|
-
});
|
|
8804
|
-
try {
|
|
8805
|
-
const r = await singlePassCompact(convText, extraction, null, rc.prevContext + rc.projectCtx, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined);
|
|
8806
|
-
finalSummary = r.summary;
|
|
8807
|
-
method = "single-pass";
|
|
8808
|
-
} catch (err) {
|
|
8809
|
-
cacheable = false;
|
|
8810
|
-
generationFallbacks.push("single-pass generation failed");
|
|
8811
|
-
debugError("Single-pass synthesis used deterministic fallback", err);
|
|
8812
|
-
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback", "info");
|
|
8813
|
-
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
8814
|
-
method = "heuristic";
|
|
8815
|
-
}
|
|
8816
|
-
} else {
|
|
8817
|
-
const needsExploration = !shouldSkipExplore && shouldExplore(extraction);
|
|
8818
|
-
if (needsExploration) {
|
|
8819
|
-
const exploreStart = Date.now();
|
|
8820
|
-
showProgressOverlay(rc.ctx, {
|
|
8821
|
-
phase: 2,
|
|
8822
|
-
phaseName: "Explore",
|
|
8823
|
-
detail: "Mapping topic shifts and continuity risks",
|
|
8824
|
-
model: rc.modelLabel,
|
|
8825
|
-
profile: rc.profile,
|
|
8826
|
-
extraction
|
|
8827
|
-
});
|
|
8828
|
-
try {
|
|
8829
|
-
const segAuth = await resolveStageAuth(rc, "explore");
|
|
8830
|
-
const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [
|
|
8831
|
-
rc.userNote,
|
|
8832
|
-
rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined
|
|
8833
|
-
].filter(Boolean).join(`
|
|
8834
|
-
`) || undefined, rc.cancellation.signal, MAX_EXPLORATION_ROUNDS, rc.notify, rc.services);
|
|
8835
|
-
explorationReport = expResult.report;
|
|
8836
|
-
explorationRounds = expResult.rounds;
|
|
8837
|
-
rc.notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
8838
|
-
rc.vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
|
|
8839
|
-
} catch (err) {
|
|
8840
|
-
cacheable = false;
|
|
8841
|
-
generationFallbacks.push("exploration unavailable");
|
|
8842
|
-
debugError("Explore used deterministic topic boundaries", err);
|
|
8843
|
-
rc.notify("Explore unavailable \xB7 using deterministic topic boundaries", "info");
|
|
8844
|
-
} finally {
|
|
8845
|
-
const exploreEnd = Date.now();
|
|
8846
|
-
markMeasuredPhase(rc, "explore", exploreStart, exploreEnd);
|
|
8847
|
-
synthPhaseStart = exploreEnd;
|
|
8848
|
-
}
|
|
8849
|
-
} else {
|
|
8850
|
-
rc.notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
|
|
8851
|
-
}
|
|
8852
|
-
let boundaries;
|
|
8853
|
-
if (explorationReport?.boundaries.length) {
|
|
8854
|
-
const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
|
|
8855
|
-
const heuristicBounds = extraction.topics.map((t) => ({
|
|
8856
|
-
afterIndex: t.endIndex,
|
|
8857
|
-
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
8858
|
-
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
8859
|
-
confidence: 0.6
|
|
8860
|
-
}));
|
|
8861
|
-
if (llmBounds.length > 0) {
|
|
8862
|
-
const merged = [...llmBounds];
|
|
8863
|
-
for (const hb of heuristicBounds) {
|
|
8864
|
-
const nearby = merged.find((m) => Math.abs(m.afterIndex - hb.afterIndex) <= 3);
|
|
8865
|
-
if (!nearby)
|
|
8866
|
-
merged.push(hb);
|
|
8867
|
-
}
|
|
8868
|
-
boundaries = merged.sort((a, b) => a.afterIndex - b.afterIndex);
|
|
8869
|
-
} else {
|
|
8870
|
-
boundaries = heuristicBounds;
|
|
8871
|
-
}
|
|
8872
|
-
} else {
|
|
8873
|
-
boundaries = extraction.topics.map((t) => ({
|
|
8874
|
-
afterIndex: t.endIndex,
|
|
8875
|
-
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
8876
|
-
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
8877
|
-
confidence: 0.6
|
|
8878
|
-
}));
|
|
8879
|
-
}
|
|
8880
|
-
const chunks = chunkLlmMessages(rc.llmMessages, boundaries, pc, rc.estimator, rc.config.focusWeighting ? rc.focus : undefined);
|
|
8881
|
-
chunkCount = chunks.length;
|
|
8882
|
-
rc.notify("Chunked: " + chunkCount + " chunks", "info");
|
|
8883
|
-
rc.vlog("Chunk topics: " + chunks.map((c) => c.topic + "[" + c.startIndex + "-" + c.endIndex + "]").join(", "));
|
|
8884
|
-
const batches = createBatches(chunks, pc.batchMaxTokens);
|
|
8885
|
-
const totalBatches = batches.length;
|
|
8886
|
-
showProgressOverlay(rc.ctx, {
|
|
8887
|
-
phase: 3,
|
|
8888
|
-
phaseName: "Synthesize",
|
|
8889
|
-
detail: "Compressing older history \xB7 batch 0/" + totalBatches,
|
|
8890
|
-
model: rc.modelLabel,
|
|
8891
|
-
profile: rc.profile,
|
|
8892
|
-
extraction,
|
|
8893
|
-
explorationRounds,
|
|
8894
|
-
totalBatches
|
|
8895
|
-
});
|
|
8896
|
-
const concurrency = rc.providerCaps.concurrencyLimit;
|
|
8897
|
-
if (totalBatches <= 1) {
|
|
8898
|
-
const single = batches[0];
|
|
8899
|
-
if (single) {
|
|
8900
|
-
if (rc.services.budget.remainingCalls() <= 1) {
|
|
8901
|
-
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
8902
|
-
cacheable = false;
|
|
8903
|
-
generationFallbacks.push("call budget reserved for final assembly");
|
|
8904
|
-
rc.notify("Call budget: chunk synthesis uses deterministic evidence so final assembly remains available", "info");
|
|
8905
|
-
} else {
|
|
8906
|
-
try {
|
|
8907
|
-
summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, single.length, rc.providerCaps.maxOutputTokens), rc.sessionId));
|
|
8908
|
-
} catch (err) {
|
|
8909
|
-
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
8910
|
-
cacheable = false;
|
|
8911
|
-
generationFallbacks.push("1 synthesis batch fallback");
|
|
8912
|
-
debugError("Synthesis batch used deterministic fallback", err);
|
|
8913
|
-
rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage", "info");
|
|
8914
|
-
showProgressOverlay(rc.ctx, {
|
|
8915
|
-
phase: 3,
|
|
8916
|
-
phaseName: "Synthesize",
|
|
8917
|
-
detail: "1 batch fallback \xB7 preserving coverage from deterministic evidence",
|
|
8918
|
-
explorationRounds
|
|
8919
|
-
});
|
|
8920
|
-
}
|
|
8921
|
-
}
|
|
8922
|
-
} else {
|
|
8923
|
-
rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
|
|
8924
|
-
}
|
|
8925
|
-
} else {
|
|
8926
|
-
const results = new Array(totalBatches);
|
|
8927
|
-
const errors = new Array(totalBatches).fill(null);
|
|
8928
|
-
const batchCallLimit = Math.max(0, Math.min(totalBatches, rc.services.budget.remainingCalls() - 1));
|
|
8929
|
-
for (let index = batchCallLimit;index < totalBatches; index++) {
|
|
8930
|
-
results[index] = batches[index].map((chunk) => failedChunkSummary(chunk));
|
|
8931
|
-
}
|
|
8932
|
-
if (batchCallLimit < totalBatches) {
|
|
8933
|
-
rc.notify("Call budget: " + (totalBatches - batchCallLimit) + " batch(es) use deterministic fallback to reserve assembly", "info");
|
|
8934
|
-
cacheable = false;
|
|
8935
|
-
generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
|
|
8936
|
-
}
|
|
8937
|
-
let completed = totalBatches - batchCallLimit;
|
|
8938
|
-
let nextBatch = 0;
|
|
8939
|
-
let budgetStopped = false;
|
|
8940
|
-
const runWorker = async () => {
|
|
8941
|
-
while (true) {
|
|
8942
|
-
const idx = nextBatch++;
|
|
8943
|
-
if (idx >= batchCallLimit)
|
|
8944
|
-
return;
|
|
8945
|
-
if (budgetStopped || rc.services.budget.reason()) {
|
|
8946
|
-
budgetStopped = true;
|
|
8947
|
-
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8948
|
-
} else {
|
|
8949
|
-
try {
|
|
8950
|
-
const batch = batches[idx];
|
|
8951
|
-
results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
|
|
8952
|
-
} catch (err) {
|
|
8953
|
-
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
8954
|
-
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
8955
|
-
}
|
|
8956
|
-
}
|
|
8957
|
-
completed++;
|
|
8958
|
-
showProgressOverlay(rc.ctx, {
|
|
8959
|
-
phase: 3,
|
|
8960
|
-
phaseName: "Synthesize",
|
|
8961
|
-
detail: "Compressing older history \xB7 batch " + completed + "/" + totalBatches,
|
|
8962
|
-
model: rc.modelLabel,
|
|
8963
|
-
profile: rc.profile,
|
|
8964
|
-
extraction,
|
|
8965
|
-
explorationRounds,
|
|
8966
|
-
totalBatches,
|
|
8967
|
-
currentBatch: completed
|
|
8968
|
-
});
|
|
8969
|
-
}
|
|
8970
|
-
};
|
|
8971
|
-
const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
|
|
8972
|
-
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
8973
|
-
if (budgetStopped) {
|
|
8974
|
-
rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
|
|
8975
|
-
cacheable = false;
|
|
8976
|
-
generationFallbacks.push("synthesis budget exhausted during batch pool");
|
|
8977
|
-
}
|
|
8978
|
-
for (const r of results)
|
|
8979
|
-
if (r)
|
|
8980
|
-
summaries.push(...r);
|
|
8981
|
-
const failedBatches = errors.filter(Boolean);
|
|
8982
|
-
for (const error2 of failedBatches)
|
|
8983
|
-
debugError("Synthesis batch used deterministic fallback", error2);
|
|
8984
|
-
if (failedBatches.length) {
|
|
8985
|
-
cacheable = false;
|
|
8986
|
-
generationFallbacks.push(failedBatches.length + " synthesis batch fallback(s)");
|
|
8987
|
-
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage", "info");
|
|
8988
|
-
showProgressOverlay(rc.ctx, {
|
|
8989
|
-
phase: 3,
|
|
8990
|
-
phaseName: "Synthesize",
|
|
8991
|
-
detail: failedBatches.length + " batch fallback(s) \xB7 preserving coverage from deterministic evidence",
|
|
8992
|
-
explorationRounds
|
|
8993
|
-
});
|
|
8994
|
-
}
|
|
8995
|
-
}
|
|
8996
|
-
showProgressOverlay(rc.ctx, {
|
|
8997
|
-
phase: 3,
|
|
8998
|
-
phaseName: "Synthesize",
|
|
8999
|
-
detail: "Merging summaries with project continuity",
|
|
9000
|
-
model: rc.modelLabel,
|
|
9001
|
-
profile: rc.profile,
|
|
9002
|
-
extraction,
|
|
9003
|
-
explorationRounds,
|
|
9004
|
-
totalBatches: batches.length
|
|
9005
|
-
});
|
|
9006
|
-
try {
|
|
9007
|
-
const r = await assembleLLM(summaries, extraction, explorationReport, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.prevContext, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined, rc.previousState);
|
|
9008
|
-
if (r?.startsWith("##"))
|
|
9009
|
-
finalSummary = r;
|
|
9010
|
-
else
|
|
9011
|
-
throw new Error("bad");
|
|
9012
|
-
} catch (err) {
|
|
9013
|
-
cacheable = false;
|
|
9014
|
-
generationFallbacks.push("assembly generation failed");
|
|
9015
|
-
debugError("Assembly used deterministic fallback", err);
|
|
9016
|
-
finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
9017
|
-
}
|
|
9018
|
-
method = "eesv";
|
|
9019
|
-
}
|
|
9020
|
-
Object.assign(rc, {
|
|
9021
|
-
finalSummary,
|
|
9022
|
-
method,
|
|
9023
|
-
methodForMetrics: method,
|
|
9024
|
-
generationFallbacks,
|
|
9025
|
-
llmCalls: rc.services.metrics.summary().totalCalls,
|
|
9026
|
-
summaries,
|
|
9027
|
-
explorationReport,
|
|
9028
|
-
explorationRounds,
|
|
9029
|
-
chunkCount
|
|
9030
|
-
});
|
|
9031
|
-
const out = advance(rc, "_synthesized");
|
|
9032
|
-
if (cacheable) {
|
|
9033
|
-
setCachedSynthesis(cacheKey, {
|
|
9034
|
-
finalSummary,
|
|
9035
|
-
method,
|
|
9036
|
-
summaries,
|
|
9037
|
-
explorationReport,
|
|
9038
|
-
explorationRounds,
|
|
9039
|
-
chunkCount
|
|
9040
|
-
});
|
|
9041
|
-
}
|
|
9042
|
-
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
9043
|
-
return out;
|
|
9044
|
-
}
|
|
9045
|
-
|
|
9046
|
-
// src/phases/verify.ts
|
|
9047
|
-
var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
|
|
9048
|
-
var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
|
|
9049
|
-
var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
9050
|
-
var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
9051
|
-
var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
|
|
9052
|
-
function noneBlockerLineIndexes(lines) {
|
|
9053
|
-
const indexes = new Set;
|
|
9054
|
-
const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
|
|
9055
|
-
for (const item of nonEmpty) {
|
|
9056
|
-
if (BULLET_NONE_BLOCKER_RE.test(item.text))
|
|
9057
|
-
indexes.add(item.index);
|
|
9058
|
-
}
|
|
9059
|
-
if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
|
|
9060
|
-
indexes.add(nonEmpty[0].index);
|
|
8888
|
+
if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
|
|
8889
|
+
indexes.add(nonEmpty[0].index);
|
|
9061
8890
|
}
|
|
9062
8891
|
return indexes;
|
|
9063
8892
|
}
|
|
@@ -9116,8 +8945,9 @@ function hasListedPath(listed, file, display, normalizedOwners) {
|
|
|
9116
8945
|
}
|
|
9117
8946
|
return false;
|
|
9118
8947
|
}
|
|
9119
|
-
function outcomeClaims(summary) {
|
|
9120
|
-
|
|
8948
|
+
function outcomeClaims(summary, pathEvidence) {
|
|
8949
|
+
const pathLines = new Set(Array.from(pathEvidence, ([path12, display]) => [path12, display, "`" + path12 + "`"]).flat());
|
|
8950
|
+
return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !pathLines.has(line)).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
|
|
9121
8951
|
}
|
|
9122
8952
|
function classifyOutcomeClaim(claim) {
|
|
9123
8953
|
const lower = claim.toLowerCase();
|
|
@@ -9416,7 +9246,7 @@ function stemToken(token) {
|
|
|
9416
9246
|
return lower;
|
|
9417
9247
|
}
|
|
9418
9248
|
function semanticTokens(text) {
|
|
9419
|
-
return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
|
|
9249
|
+
return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2 || NEGATION_MARKERS.has(token));
|
|
9420
9250
|
}
|
|
9421
9251
|
var semanticShapeCache = new Map;
|
|
9422
9252
|
var semanticFragmentCache = new Map;
|
|
@@ -9431,574 +9261,1022 @@ function semanticFragments(text) {
|
|
|
9431
9261
|
function hasNearbyMarker(tokens, anchor, markers) {
|
|
9432
9262
|
return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
|
|
9433
9263
|
}
|
|
9434
|
-
function hasEffectiveTargetNegation(tokens, anchor) {
|
|
9435
|
-
return tokens.some((token, anchorIndex) => {
|
|
9436
|
-
if (token !== anchor)
|
|
9437
|
-
return false;
|
|
9438
|
-
const nearbyStart = Math.max(0, anchorIndex - 2);
|
|
9439
|
-
const nearbyNegations = tokens.slice(nearbyStart, anchorIndex + 3).map((near, offset) => NEGATION_MARKERS.has(near) ? nearbyStart + offset : -1).filter((index) => index >= 0);
|
|
9440
|
-
const governingStart = Math.max(0, anchorIndex - 3);
|
|
9441
|
-
const preceding = tokens.slice(governingStart, anchorIndex);
|
|
9442
|
-
const nearbyGuards = preceding.map((near, offset) => POLARITY_INVERTING_GUARDS.has(near) ? governingStart + offset : -1).filter((index) => index >= 0);
|
|
9443
|
-
const governingIndex = preceding.findIndex((near, offset) => NEGATION_MARKERS.has(near) && POLARITY_INVERTING_GUARDS.has(preceding[offset + 1] ?? ""));
|
|
9444
|
-
if (governingIndex < 0)
|
|
9445
|
-
return nearbyNegations.length > 0 || nearbyGuards.length > 0;
|
|
9446
|
-
const absoluteGoverningIndex = governingStart + governingIndex;
|
|
9447
|
-
const guardIndex = absoluteGoverningIndex + 1;
|
|
9448
|
-
const nested = tokens.slice(guardIndex + 1, anchorIndex).some((inner) => NEGATION_MARKERS.has(inner) || POLARITY_INVERTING_GUARDS.has(inner));
|
|
9449
|
-
return nested || nearbyNegations.some((index) => index !== absoluteGoverningIndex && index !== guardIndex) || nearbyGuards.some((index) => index !== guardIndex);
|
|
9450
|
-
});
|
|
9264
|
+
function hasEffectiveTargetNegation(tokens, anchor) {
|
|
9265
|
+
return tokens.some((token, anchorIndex) => {
|
|
9266
|
+
if (token !== anchor)
|
|
9267
|
+
return false;
|
|
9268
|
+
const nearbyStart = Math.max(0, anchorIndex - 2);
|
|
9269
|
+
const nearbyNegations = tokens.slice(nearbyStart, anchorIndex + 3).map((near, offset) => NEGATION_MARKERS.has(near) && !(near === "without" && nearbyStart + offset > anchorIndex) ? nearbyStart + offset : -1).filter((index) => index >= 0);
|
|
9270
|
+
const governingStart = Math.max(0, anchorIndex - 3);
|
|
9271
|
+
const preceding = tokens.slice(governingStart, anchorIndex);
|
|
9272
|
+
const nearbyGuards = preceding.map((near, offset) => POLARITY_INVERTING_GUARDS.has(near) ? governingStart + offset : -1).filter((index) => index >= 0);
|
|
9273
|
+
const governingIndex = preceding.findIndex((near, offset) => NEGATION_MARKERS.has(near) && POLARITY_INVERTING_GUARDS.has(preceding[offset + 1] ?? ""));
|
|
9274
|
+
if (governingIndex < 0)
|
|
9275
|
+
return nearbyNegations.length > 0 || nearbyGuards.length > 0;
|
|
9276
|
+
const absoluteGoverningIndex = governingStart + governingIndex;
|
|
9277
|
+
const guardIndex = absoluteGoverningIndex + 1;
|
|
9278
|
+
const nested = tokens.slice(guardIndex + 1, anchorIndex).some((inner) => NEGATION_MARKERS.has(inner) || POLARITY_INVERTING_GUARDS.has(inner));
|
|
9279
|
+
return nested || nearbyNegations.some((index) => index !== absoluteGoverningIndex && index !== guardIndex) || nearbyGuards.some((index) => index !== guardIndex);
|
|
9280
|
+
});
|
|
9281
|
+
}
|
|
9282
|
+
function semanticShape(source) {
|
|
9283
|
+
const cached = lruGet(semanticShapeCache, source);
|
|
9284
|
+
if (cached)
|
|
9285
|
+
return cached;
|
|
9286
|
+
const sourceTokens = semanticTokens(source);
|
|
9287
|
+
const concepts = Array.from(new Set(sourceTokens.filter((token) => !/^\d+$/.test(token) && !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
|
|
9288
|
+
const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
|
|
9289
|
+
const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
|
|
9290
|
+
const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
|
|
9291
|
+
const shape = { sourceTokens, concepts, anchor, negative, conditional };
|
|
9292
|
+
lruSet(semanticShapeCache, source, shape, 512);
|
|
9293
|
+
return shape;
|
|
9294
|
+
}
|
|
9295
|
+
function hasSemanticEvidence(source, target) {
|
|
9296
|
+
const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
|
|
9297
|
+
if (!concepts.length)
|
|
9298
|
+
return true;
|
|
9299
|
+
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
9300
|
+
return semanticFragments(target).some((tokens) => {
|
|
9301
|
+
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
9302
|
+
if (overlap < required)
|
|
9303
|
+
return false;
|
|
9304
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
9305
|
+
if (negative && !targetNegative) {
|
|
9306
|
+
const conditionalRestatement = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
9307
|
+
if (!conditionalRestatement)
|
|
9308
|
+
return false;
|
|
9309
|
+
}
|
|
9310
|
+
if (!negative && targetNegative)
|
|
9311
|
+
return false;
|
|
9312
|
+
if (conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token)))
|
|
9313
|
+
return false;
|
|
9314
|
+
return true;
|
|
9315
|
+
});
|
|
9316
|
+
}
|
|
9317
|
+
function hasSemanticContradiction(source, target) {
|
|
9318
|
+
const sourceFragments = new Set(semanticFragments(source).map((tokens) => tokens.join(" ")));
|
|
9319
|
+
const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
|
|
9320
|
+
if (!anchor)
|
|
9321
|
+
return false;
|
|
9322
|
+
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
9323
|
+
return semanticFragments(target).some((tokens) => {
|
|
9324
|
+
if (!tokens.includes(anchor) || sourceFragments.has(tokens.join(" ")))
|
|
9325
|
+
return false;
|
|
9326
|
+
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
9327
|
+
if (overlap < required)
|
|
9328
|
+
return false;
|
|
9329
|
+
const targetNegative = hasEffectiveTargetNegation(tokens, anchor);
|
|
9330
|
+
if (negative && !targetNegative) {
|
|
9331
|
+
const validConditional = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
9332
|
+
return !validConditional;
|
|
9333
|
+
}
|
|
9334
|
+
if (!negative && targetNegative)
|
|
9335
|
+
return true;
|
|
9336
|
+
return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
|
|
9337
|
+
});
|
|
9338
|
+
}
|
|
9339
|
+
function isDeterministicallyPatchable(gap) {
|
|
9340
|
+
if (gap.kind === "fabricated-file")
|
|
9341
|
+
return true;
|
|
9342
|
+
if (gap.kind === "inconsistency")
|
|
9343
|
+
return gap.detail.startsWith("blocked-none:");
|
|
9344
|
+
return true;
|
|
9345
|
+
}
|
|
9346
|
+
function repairSummaryDeterministically(summary, result, extraction, continuity = null, evidence = {}, maxRounds = 3) {
|
|
9347
|
+
const patched = [];
|
|
9348
|
+
const seen = new Set;
|
|
9349
|
+
for (let round = 0;round < maxRounds; round++) {
|
|
9350
|
+
const patchable = result.gaps.filter(isDeterministicallyPatchable);
|
|
9351
|
+
if (!patchable.length)
|
|
9352
|
+
break;
|
|
9353
|
+
const next = patchDeterministic(summary, patchable, extraction, continuity, evidence);
|
|
9354
|
+
if (next === summary)
|
|
9355
|
+
break;
|
|
9356
|
+
for (const gap of patchable) {
|
|
9357
|
+
const key = formatVerificationGap(gap);
|
|
9358
|
+
if (!seen.has(key)) {
|
|
9359
|
+
seen.add(key);
|
|
9360
|
+
patched.push(gap);
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9363
|
+
summary = next;
|
|
9364
|
+
result = verifySummary(summary, extraction, continuity, evidence);
|
|
9365
|
+
}
|
|
9366
|
+
return { summary, result, patched };
|
|
9367
|
+
}
|
|
9368
|
+
function addGap(accumulator, gap, penalty) {
|
|
9369
|
+
accumulator.gaps.push(gap);
|
|
9370
|
+
accumulator.score -= penalty;
|
|
9371
|
+
}
|
|
9372
|
+
function uniqueByText(items, text) {
|
|
9373
|
+
const seen = new Set;
|
|
9374
|
+
return items.filter((item) => {
|
|
9375
|
+
const key = text(item).toLowerCase().replace(/\s+/g, " ").trim();
|
|
9376
|
+
if (!key || seen.has(key))
|
|
9377
|
+
return false;
|
|
9378
|
+
seen.add(key);
|
|
9379
|
+
return true;
|
|
9380
|
+
});
|
|
9381
|
+
}
|
|
9382
|
+
function collectVerificationEvidence(extraction, continuity, evidence) {
|
|
9383
|
+
const unresolved = uniqueByText([
|
|
9384
|
+
...extraction.errors.flatMap((error2) => !error2.resolved ? [{ message: error2.message }] : []),
|
|
9385
|
+
...(continuity?.unresolvedErrors ?? []).map((error2) => ({
|
|
9386
|
+
message: error2.message
|
|
9387
|
+
}))
|
|
9388
|
+
], (item) => item.message);
|
|
9389
|
+
const resolved = uniqueByText([
|
|
9390
|
+
...extraction.errors.flatMap((error2) => error2.resolved ? [{ message: error2.message }] : []),
|
|
9391
|
+
...(continuity?.resolvedErrors ?? []).map((error2) => ({
|
|
9392
|
+
message: error2.message
|
|
9393
|
+
}))
|
|
9394
|
+
], (item) => item.message).slice(-5);
|
|
9395
|
+
const steeringConstraints = [];
|
|
9396
|
+
if (evidence.steering?.focus?.trim()) {
|
|
9397
|
+
steeringConstraints.push({
|
|
9398
|
+
text: "Preserve detail about: " + evidence.steering.focus
|
|
9399
|
+
});
|
|
9400
|
+
}
|
|
9401
|
+
if (evidence.steering?.note?.trim()) {
|
|
9402
|
+
steeringConstraints.push({ text: evidence.steering.note });
|
|
9403
|
+
}
|
|
9404
|
+
const constraints = uniqueByText([
|
|
9405
|
+
...extraction.constraints.flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
9406
|
+
...(continuity?.constraints ?? []).flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
9407
|
+
...steeringConstraints
|
|
9408
|
+
], (item) => item.text).filter((item) => !isDiagnosticConstraintText(item.text));
|
|
9409
|
+
const decisions = uniqueByText([
|
|
9410
|
+
...extraction.decisions.flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : []),
|
|
9411
|
+
...(continuity?.decisions ?? []).flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : [])
|
|
9412
|
+
], (item) => item.summary);
|
|
9413
|
+
return {
|
|
9414
|
+
unresolved,
|
|
9415
|
+
resolved,
|
|
9416
|
+
constraints,
|
|
9417
|
+
decisions,
|
|
9418
|
+
goal: extraction.mainGoal ?? continuity?.goal ?? null
|
|
9419
|
+
};
|
|
9420
|
+
}
|
|
9421
|
+
function verifyRequiredSections(parsed, accumulator) {
|
|
9422
|
+
const required = [
|
|
9423
|
+
{ kind: "goal", penalty: 5 },
|
|
9424
|
+
{ kind: "progress", penalty: 5 },
|
|
9425
|
+
{ kind: "critical-context", penalty: 3 }
|
|
9426
|
+
];
|
|
9427
|
+
for (const item of required) {
|
|
9428
|
+
if (!findSection(parsed, item.kind)) {
|
|
9429
|
+
addGap(accumulator, { kind: "missing-section", section: item.kind }, item.penalty);
|
|
9430
|
+
}
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9433
|
+
function verifyPathCoverage(parsed, extraction, continuity, evidence, accumulator) {
|
|
9434
|
+
const modified = extraction.modifiedFiles.map((file) => file.path);
|
|
9435
|
+
const read = extraction.readFiles;
|
|
9436
|
+
const deleted = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
|
|
9437
|
+
const required = [...modified, ...read, ...deleted];
|
|
9438
|
+
const expected = new Set(required);
|
|
9439
|
+
const rendered = buildSummaryPathEvidence(required, evidence.summaryBudgetTokens);
|
|
9440
|
+
const ownerSets = new Map;
|
|
9441
|
+
for (const file of required) {
|
|
9442
|
+
const display = rendered.get(file);
|
|
9443
|
+
for (const candidate of [
|
|
9444
|
+
file,
|
|
9445
|
+
...display ? [decodePathDisplay(display)] : []
|
|
9446
|
+
]) {
|
|
9447
|
+
const normalized = normalizePath(candidate);
|
|
9448
|
+
const owners = ownerSets.get(normalized) ?? new Set;
|
|
9449
|
+
owners.add(file);
|
|
9450
|
+
ownerSets.set(normalized, owners);
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9453
|
+
const ownerCounts = new Map(Array.from(ownerSets, ([file, owners]) => [file, owners.size]));
|
|
9454
|
+
const listed = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expected);
|
|
9455
|
+
const modifiedListed = listed("files-modified");
|
|
9456
|
+
const readListed = listed("files-read");
|
|
9457
|
+
const deletedListed = listed("files-deleted");
|
|
9458
|
+
for (const file of modified) {
|
|
9459
|
+
const display = rendered.get(file);
|
|
9460
|
+
if (display && !hasListedPath(modifiedListed, file, display, ownerCounts)) {
|
|
9461
|
+
addGap(accumulator, { kind: "missing-file", path: file }, 5);
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
for (const file of read) {
|
|
9465
|
+
const display = rendered.get(file);
|
|
9466
|
+
if (display && !hasListedPath(readListed, file, display, ownerCounts)) {
|
|
9467
|
+
addGap(accumulator, { kind: "missing-read-file", path: file }, 5);
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
for (const file of deleted) {
|
|
9471
|
+
const display = rendered.get(file);
|
|
9472
|
+
if (display && !hasListedPath(deletedListed, file, display, ownerCounts)) {
|
|
9473
|
+
addGap(accumulator, { kind: "missing-deleted-file", path: file }, 5);
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
return { modified, read, deleted, rendered };
|
|
9477
|
+
}
|
|
9478
|
+
function verifyErrorEvidence(normalizedSummary, collected, accumulator) {
|
|
9479
|
+
for (const error2 of collected.unresolved) {
|
|
9480
|
+
const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
9481
|
+
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
9482
|
+
addGap(accumulator, { kind: "missing-error", message: error2.message }, 5);
|
|
9483
|
+
}
|
|
9484
|
+
}
|
|
9485
|
+
for (const error2 of collected.resolved) {
|
|
9486
|
+
const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
9487
|
+
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
9488
|
+
addGap(accumulator, { kind: "missing-error", message: error2.message, resolved: true }, 2);
|
|
9489
|
+
}
|
|
9490
|
+
}
|
|
9491
|
+
}
|
|
9492
|
+
function verifySemanticCoverage(parsed, collected, accumulator) {
|
|
9493
|
+
const constraintTarget = [
|
|
9494
|
+
findSection(parsed, "constraints")?.body ?? "",
|
|
9495
|
+
findSection(parsed, "critical-context")?.body ?? ""
|
|
9496
|
+
].join(`
|
|
9497
|
+
`);
|
|
9498
|
+
for (const constraint of collected.constraints) {
|
|
9499
|
+
if (!hasSemanticEvidence(constraint.text, constraintTarget)) {
|
|
9500
|
+
addGap(accumulator, { kind: "missing-constraint", text: constraint.text }, 8);
|
|
9501
|
+
}
|
|
9502
|
+
if (hasSemanticContradiction(constraint.text, constraintTarget)) {
|
|
9503
|
+
addGap(accumulator, {
|
|
9504
|
+
kind: "inconsistency",
|
|
9505
|
+
detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
|
|
9506
|
+
}, 20);
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
if (collected.goal) {
|
|
9510
|
+
const goalTarget = findSection(parsed, "goal")?.body ?? "";
|
|
9511
|
+
if (!hasSemanticEvidence(collected.goal, goalTarget)) {
|
|
9512
|
+
addGap(accumulator, { kind: "missing-goal", goal: collected.goal }, 12);
|
|
9513
|
+
}
|
|
9514
|
+
if (hasSemanticContradiction(collected.goal, goalTarget)) {
|
|
9515
|
+
addGap(accumulator, {
|
|
9516
|
+
kind: "inconsistency",
|
|
9517
|
+
detail: "semantic-contradiction: goal polarity or condition changed"
|
|
9518
|
+
}, 20);
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
const decisionBody = findSection(parsed, "decisions")?.body ?? "";
|
|
9522
|
+
for (const decision of collected.decisions) {
|
|
9523
|
+
if (!hasSemanticEvidence(decision.summary, decisionBody)) {
|
|
9524
|
+
addGap(accumulator, { kind: "missing-decision", summary: decision.summary }, 8);
|
|
9525
|
+
}
|
|
9526
|
+
if (hasSemanticContradiction(decision.summary, decisionBody)) {
|
|
9527
|
+
addGap(accumulator, {
|
|
9528
|
+
kind: "inconsistency",
|
|
9529
|
+
detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
9530
|
+
}, 20);
|
|
9531
|
+
}
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
function verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator) {
|
|
9535
|
+
const groundedEvidence = [
|
|
9536
|
+
...collected.unresolved.map((item) => item.message),
|
|
9537
|
+
...collected.resolved.map((item) => item.message),
|
|
9538
|
+
...collected.constraints.map((item) => item.text),
|
|
9539
|
+
...collected.decisions.map((item) => item.summary),
|
|
9540
|
+
...collected.goal ? [collected.goal] : [],
|
|
9541
|
+
...extraction.lastUserMessages,
|
|
9542
|
+
...extraction.timeline.map((item) => item.summary),
|
|
9543
|
+
...extraction.topics.map((item) => item.primaryFile ?? ""),
|
|
9544
|
+
...continuity?.openLoops.map((item) => item.summary) ?? [],
|
|
9545
|
+
...continuity?.criticalContext ?? []
|
|
9546
|
+
];
|
|
9547
|
+
const groundedFiles = groundedEvidence.flatMap((value) => [
|
|
9548
|
+
value,
|
|
9549
|
+
summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
|
|
9550
|
+
summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
|
|
9551
|
+
summaryEvidenceLine(value, TRUNC.PREVIEW),
|
|
9552
|
+
summaryEvidenceLine(value, TRUNC.MESSAGE)
|
|
9553
|
+
]).flatMap(extractFileRefs);
|
|
9554
|
+
const renderedPaths = Array.from(paths.rendered.values()).flatMap((line) => [
|
|
9555
|
+
decodePathDisplay(line),
|
|
9556
|
+
line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
|
|
9557
|
+
]);
|
|
9558
|
+
const knownFiles = Array.from(new Set([
|
|
9559
|
+
...paths.modified,
|
|
9560
|
+
...paths.read,
|
|
9561
|
+
...paths.deleted,
|
|
9562
|
+
...extraction.referencedFiles ?? [],
|
|
9563
|
+
...groundedFiles,
|
|
9564
|
+
...renderedPaths,
|
|
9565
|
+
...renderedPaths.flatMap(extractFileRefs),
|
|
9566
|
+
...continuity?.modifiedFiles ?? [],
|
|
9567
|
+
...continuity?.readFiles ?? [],
|
|
9568
|
+
...(continuity?.unresolvedErrors ?? []).flatMap((error2) => error2.files),
|
|
9569
|
+
...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
|
|
9570
|
+
]));
|
|
9571
|
+
const knownFileIndex = buildKnownPathReferenceIndex(knownFiles);
|
|
9572
|
+
for (const ref of new Set(extractFileRefs(summary))) {
|
|
9573
|
+
const grounded = isKnownPathReferenceInIndex(ref, knownFileIndex) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
|
|
9574
|
+
if (!grounded)
|
|
9575
|
+
addGap(accumulator, { kind: "fabricated-file", ref }, 4);
|
|
9576
|
+
}
|
|
9451
9577
|
}
|
|
9452
|
-
function
|
|
9453
|
-
const
|
|
9454
|
-
if (
|
|
9455
|
-
return
|
|
9456
|
-
const
|
|
9457
|
-
const
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9578
|
+
function verifyProgressConsistency(parsed, extraction, collected, paths, accumulator) {
|
|
9579
|
+
const progress = findSection(parsed, "progress");
|
|
9580
|
+
if (!progress)
|
|
9581
|
+
return;
|
|
9582
|
+
const done = progress.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
9583
|
+
const blocked = progress.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
9584
|
+
if (collected.unresolved.length > 0 && noneBlockerLineIndexes(blocked.split(/\r?\n/).slice(1)).size > 0) {
|
|
9585
|
+
addGap(accumulator, {
|
|
9586
|
+
kind: "inconsistency",
|
|
9587
|
+
detail: "blocked-none: Blocked says none despite unresolved errors"
|
|
9588
|
+
}, 12);
|
|
9589
|
+
}
|
|
9590
|
+
const doneRefs = new Set(extractFileRefs(done).map(normalizePath));
|
|
9591
|
+
const modifiedPathOwners = buildPathNeedleOwnershipIndex(paths.modified);
|
|
9592
|
+
for (const file of extraction.modifiedFiles) {
|
|
9593
|
+
const needles = buildUniquePathNeedlesFromIndex(file.path, modifiedPathOwners);
|
|
9594
|
+
if (!needles.some((needle) => doneRefs.has(needle)))
|
|
9595
|
+
continue;
|
|
9596
|
+
const unresolved = collected.unresolved.find((error2) => {
|
|
9597
|
+
const firstLine = error2.message.split(/\r?\n/, 1)[0] ?? "";
|
|
9598
|
+
const refs = extractFileRefs(firstLine).map(normalizePath);
|
|
9599
|
+
return needles.some((needle) => refs.includes(normalizePath(needle)));
|
|
9600
|
+
});
|
|
9601
|
+
if (unresolved) {
|
|
9602
|
+
addGap(accumulator, {
|
|
9603
|
+
kind: "inconsistency",
|
|
9604
|
+
detail: file.path + " marked Done but has unresolved error"
|
|
9605
|
+
}, 5);
|
|
9606
|
+
}
|
|
9607
|
+
}
|
|
9464
9608
|
}
|
|
9465
|
-
function
|
|
9466
|
-
const
|
|
9467
|
-
if (!
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
const conditionalRestatement = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
|
|
9477
|
-
if (!conditionalRestatement)
|
|
9478
|
-
return false;
|
|
9609
|
+
function verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity, evidence, collected, accumulator) {
|
|
9610
|
+
const unresolvedCount = collected.unresolved.length + (continuity?.openLoops.filter((loop) => loop.status !== "resolved").length ?? 0);
|
|
9611
|
+
if (unresolvedCount >= 1 && !findSection(parsed, "open-loops") && !summary.toLowerCase().replace(/\\/g, "/").includes("unresolved")) {
|
|
9612
|
+
addGap(accumulator, { kind: "missing-open-loops", unresolvedCount }, 5);
|
|
9613
|
+
}
|
|
9614
|
+
if (!evidence.sourceMessages)
|
|
9615
|
+
return;
|
|
9616
|
+
const tools = successfulToolEvidence(evidence.sourceMessages);
|
|
9617
|
+
for (const claim of outcomeClaims(summary, paths.rendered)) {
|
|
9618
|
+
if (!successfulToolSupportsClaim(claim, tools, extraction)) {
|
|
9619
|
+
addGap(accumulator, { kind: "unsupported-claim", claim }, 20);
|
|
9479
9620
|
}
|
|
9480
|
-
|
|
9481
|
-
return false;
|
|
9482
|
-
if (conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token)))
|
|
9483
|
-
return false;
|
|
9484
|
-
return true;
|
|
9485
|
-
});
|
|
9621
|
+
}
|
|
9486
9622
|
}
|
|
9487
|
-
function
|
|
9488
|
-
const
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9623
|
+
function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
9624
|
+
const parsed = parseSummary(summary);
|
|
9625
|
+
const accumulator = { gaps: [], score: 100 };
|
|
9626
|
+
const collected = collectVerificationEvidence(extraction, continuity, evidence);
|
|
9627
|
+
verifyRequiredSections(parsed, accumulator);
|
|
9628
|
+
const paths = verifyPathCoverage(parsed, extraction, continuity, evidence, accumulator);
|
|
9629
|
+
verifyErrorEvidence(summary.toLowerCase().replace(/\\/g, "/").replace(/\s+/g, " "), collected, accumulator);
|
|
9630
|
+
verifySemanticCoverage(parsed, collected, accumulator);
|
|
9631
|
+
verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator);
|
|
9632
|
+
verifyProgressConsistency(parsed, extraction, collected, paths, accumulator);
|
|
9633
|
+
verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity, evidence, collected, accumulator);
|
|
9634
|
+
const score = Math.max(0, accumulator.score);
|
|
9635
|
+
return {
|
|
9636
|
+
ok: accumulator.gaps.length === 0 && score >= 85,
|
|
9637
|
+
gaps: accumulator.gaps,
|
|
9638
|
+
score
|
|
9639
|
+
};
|
|
9640
|
+
}
|
|
9641
|
+
function patchDeterministic(summary, gaps, extraction, continuity = null, evidence = {}) {
|
|
9642
|
+
let canonical = parseSummary(summary);
|
|
9643
|
+
const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
|
|
9644
|
+
const readPaths = extraction.readFiles;
|
|
9645
|
+
const deletedPaths = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
|
|
9646
|
+
const pathEvidence = buildSummaryPathEvidence([...modifiedPaths, ...readPaths, ...deletedPaths], evidence.summaryBudgetTokens);
|
|
9647
|
+
const replaceFileSection = (kind, paths) => {
|
|
9648
|
+
const body = paths.map((path12) => "- " + (pathEvidence.get(path12) ?? JSON.stringify(path12))).join(`
|
|
9649
|
+
`);
|
|
9650
|
+
canonical = upsertSection(canonical, kind, body || "- None recorded.");
|
|
9651
|
+
};
|
|
9652
|
+
const safe = (value, max = TRUNC.MESSAGE) => summaryEvidenceLine(value, max);
|
|
9653
|
+
const unresolvedMessages = Array.from(new Set([
|
|
9654
|
+
...extraction.errors.filter((error2) => !error2.resolved).map((error2) => error2.message),
|
|
9655
|
+
...(continuity?.unresolvedErrors ?? []).map((error2) => error2.message)
|
|
9656
|
+
]));
|
|
9657
|
+
const unresolvedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved");
|
|
9658
|
+
const blockedItems = [
|
|
9659
|
+
...unresolvedMessages.map((message) => safe(message)).filter(Boolean).map((message) => "- " + message),
|
|
9660
|
+
...unresolvedLoops.map((loop) => safe(loop.summary)).filter(Boolean).map((summary2) => "- " + summary2)
|
|
9661
|
+
];
|
|
9662
|
+
const patchBlockedNone = () => {
|
|
9663
|
+
const progress = findSection(canonical, "progress");
|
|
9664
|
+
if (!progress || !blockedItems.length)
|
|
9665
|
+
return;
|
|
9666
|
+
const lines = progress.body.split(/\r?\n/);
|
|
9667
|
+
const start = lines.findIndex((line) => /^###\s*Blocked\s*$/i.test(line.trim()));
|
|
9668
|
+
if (start < 0)
|
|
9669
|
+
return;
|
|
9670
|
+
let end = lines.findIndex((line, index) => index > start && /^###\s+/.test(line.trim()));
|
|
9671
|
+
if (end < 0)
|
|
9672
|
+
end = lines.length;
|
|
9673
|
+
const existing = lines.slice(start + 1, end);
|
|
9674
|
+
const noneIndexes = noneBlockerLineIndexes(existing);
|
|
9675
|
+
if (!noneIndexes.size)
|
|
9676
|
+
return;
|
|
9677
|
+
const replacement = Array.from(new Set([
|
|
9678
|
+
...blockedItems,
|
|
9679
|
+
...existing.filter((line, index) => line.trim() && !noneIndexes.has(index))
|
|
9680
|
+
]));
|
|
9681
|
+
lines.splice(start + 1, end - start - 1, ...replacement);
|
|
9682
|
+
canonical = upsertSection(canonical, "progress", lines.join(`
|
|
9683
|
+
`));
|
|
9684
|
+
};
|
|
9685
|
+
for (const gap of gaps) {
|
|
9686
|
+
switch (gap.kind) {
|
|
9687
|
+
case "missing-section": {
|
|
9688
|
+
if (gap.section === "goal") {
|
|
9689
|
+
canonical = upsertSection(canonical, "goal", safe(extraction.mainGoal ?? "", TRUNC.DETAIL) || "Continue the current coding task.");
|
|
9690
|
+
} else if (gap.section === "progress") {
|
|
9691
|
+
canonical = upsertSection(canonical, "progress", `### Done
|
|
9692
|
+
- No explicit completion recorded.
|
|
9693
|
+
### In Progress
|
|
9694
|
+
- Continue from the latest user request.
|
|
9695
|
+
### Blocked
|
|
9696
|
+
` + (blockedItems.join(`
|
|
9697
|
+
`) || "- None recorded."));
|
|
9698
|
+
} else if (gap.section === "critical-context") {
|
|
9699
|
+
const critical = unresolvedMessages.flatMap((message) => {
|
|
9700
|
+
const text = safe(message);
|
|
9701
|
+
return text ? ["- Unresolved error: " + text] : [];
|
|
9702
|
+
});
|
|
9703
|
+
canonical = upsertSection(canonical, "critical-context", critical.join(`
|
|
9704
|
+
`) || "- None recorded.");
|
|
9705
|
+
}
|
|
9706
|
+
break;
|
|
9707
|
+
}
|
|
9708
|
+
case "missing-file":
|
|
9709
|
+
replaceFileSection("files-modified", modifiedPaths);
|
|
9710
|
+
break;
|
|
9711
|
+
case "missing-read-file":
|
|
9712
|
+
replaceFileSection("files-read", readPaths);
|
|
9713
|
+
break;
|
|
9714
|
+
case "missing-deleted-file":
|
|
9715
|
+
replaceFileSection("files-deleted", deletedPaths);
|
|
9716
|
+
break;
|
|
9717
|
+
case "missing-error": {
|
|
9718
|
+
const existing = findSection(canonical, "critical-context")?.body.toLowerCase() ?? "";
|
|
9719
|
+
const message = safe(gap.message);
|
|
9720
|
+
if (!existing.includes(message.toLowerCase())) {
|
|
9721
|
+
canonical = appendToSection(canonical, "critical-context", "- " + (gap.resolved ? "Resolved error: " : "Unresolved error: ") + message);
|
|
9722
|
+
}
|
|
9723
|
+
break;
|
|
9724
|
+
}
|
|
9725
|
+
case "missing-constraint":
|
|
9726
|
+
canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
|
|
9727
|
+
break;
|
|
9728
|
+
case "missing-decision":
|
|
9729
|
+
canonical = appendToSection(canonical, "decisions", "- **" + safe(gap.summary, TRUNC.DECISION_SUMMARY) + "**");
|
|
9730
|
+
break;
|
|
9731
|
+
case "missing-goal":
|
|
9732
|
+
canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
|
|
9733
|
+
break;
|
|
9734
|
+
case "missing-open-loops": {
|
|
9735
|
+
const current = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
|
|
9736
|
+
const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
|
|
9737
|
+
const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({
|
|
9738
|
+
priority: loop.priority,
|
|
9739
|
+
summary: safe(loop.summary, TRUNC.SNIPPET)
|
|
9740
|
+
})).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
|
|
9741
|
+
const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
|
|
9742
|
+
`);
|
|
9743
|
+
canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
|
|
9744
|
+
break;
|
|
9745
|
+
}
|
|
9746
|
+
case "fabricated-file": {
|
|
9747
|
+
const normalizedRef = gap.ref.replace(/\\/g, "/").toLowerCase();
|
|
9748
|
+
canonical = {
|
|
9749
|
+
sections: canonical.sections.map((section) => ({
|
|
9750
|
+
...section,
|
|
9751
|
+
body: section.body.split(`
|
|
9752
|
+
`).filter((line) => {
|
|
9753
|
+
if (!/^\s*[-*]\s+/.test(line))
|
|
9754
|
+
return true;
|
|
9755
|
+
const matches = extractFileRefs(line).some((ref) => ref.replace(/\\/g, "/").toLowerCase() === normalizedRef);
|
|
9756
|
+
return !matches;
|
|
9757
|
+
}).join(`
|
|
9758
|
+
`).trim()
|
|
9759
|
+
}))
|
|
9760
|
+
};
|
|
9761
|
+
break;
|
|
9762
|
+
}
|
|
9763
|
+
case "unsupported-claim":
|
|
9764
|
+
canonical = removeUnsupportedClaim(canonical, gap.claim);
|
|
9765
|
+
break;
|
|
9766
|
+
case "inconsistency":
|
|
9767
|
+
if (gap.detail.startsWith("blocked-none:"))
|
|
9768
|
+
patchBlockedNone();
|
|
9769
|
+
break;
|
|
9502
9770
|
}
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
|
|
9506
|
-
});
|
|
9507
|
-
}
|
|
9508
|
-
function isDeterministicallyPatchable(gap) {
|
|
9509
|
-
if (gap.kind === "fabricated-file")
|
|
9510
|
-
return true;
|
|
9511
|
-
if (gap.kind === "inconsistency")
|
|
9512
|
-
return gap.detail.startsWith("blocked-none:");
|
|
9513
|
-
return true;
|
|
9771
|
+
}
|
|
9772
|
+
return renderSummary(canonical, { canonicalHeadings: true });
|
|
9514
9773
|
}
|
|
9515
|
-
function
|
|
9516
|
-
|
|
9517
|
-
const
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
const key = formatVerificationGap(gap);
|
|
9527
|
-
if (!seen.has(key)) {
|
|
9528
|
-
seen.add(key);
|
|
9529
|
-
patched.push(gap);
|
|
9530
|
-
}
|
|
9774
|
+
function hasUnclosedMarkdownFence(markdown) {
|
|
9775
|
+
let open = null;
|
|
9776
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
9777
|
+
const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
9778
|
+
if (!match)
|
|
9779
|
+
continue;
|
|
9780
|
+
const marker = match[1][0];
|
|
9781
|
+
if (!open) {
|
|
9782
|
+
open = { marker, length: match[1].length };
|
|
9783
|
+
} else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
|
|
9784
|
+
open = null;
|
|
9531
9785
|
}
|
|
9532
|
-
summary = next;
|
|
9533
|
-
result = verifySummary(summary, extraction, continuity, evidence);
|
|
9534
9786
|
}
|
|
9535
|
-
return
|
|
9787
|
+
return open !== null;
|
|
9536
9788
|
}
|
|
9537
|
-
function
|
|
9538
|
-
|
|
9539
|
-
|
|
9789
|
+
function patchResponseIsTruncated(patched, stopReason) {
|
|
9790
|
+
const reason = String(stopReason ?? "");
|
|
9791
|
+
return /(?:length|truncat|max(?:imum)?[_ -]?(?:output[_ -]?)?tokens?|token[_ -]?limit)/i.test(reason) || /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
|
|
9540
9792
|
}
|
|
9541
|
-
function
|
|
9542
|
-
|
|
9543
|
-
return items.filter((item) => {
|
|
9544
|
-
const key = text(item).toLowerCase().replace(/\s+/g, " ").trim();
|
|
9545
|
-
if (!key || seen.has(key))
|
|
9546
|
-
return false;
|
|
9547
|
-
seen.add(key);
|
|
9548
|
-
return true;
|
|
9549
|
-
});
|
|
9793
|
+
function sectionIdentity(section) {
|
|
9794
|
+
return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
|
|
9550
9795
|
}
|
|
9551
|
-
function
|
|
9552
|
-
const
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9796
|
+
async function patchSummary(summary, gaps, model, auth, signal, services) {
|
|
9797
|
+
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.
|
|
9798
|
+
|
|
9799
|
+
Findings:
|
|
9800
|
+
` + gaps.map((gap, index) => index + 1 + ". " + formatVerificationGap(gap)).join(`
|
|
9801
|
+
`) + `
|
|
9802
|
+
|
|
9803
|
+
Current summary:
|
|
9804
|
+
` + summary + `
|
|
9805
|
+
|
|
9806
|
+
Return the COMPLETE corrected summary in the same format.`;
|
|
9807
|
+
try {
|
|
9808
|
+
const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
|
|
9809
|
+
const response = await trackedComplete("patch", model, {
|
|
9810
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
9811
|
+
messages: [
|
|
9812
|
+
{
|
|
9813
|
+
role: "user",
|
|
9814
|
+
content: [{ type: "text", text: patchPrompt }],
|
|
9815
|
+
timestamp: Date.now()
|
|
9816
|
+
}
|
|
9817
|
+
]
|
|
9818
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
|
|
9819
|
+
const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
|
|
9820
|
+
`).trim();
|
|
9821
|
+
if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
|
|
9822
|
+
return summary;
|
|
9823
|
+
const originalSections = parseSummary(summary).sections;
|
|
9824
|
+
const patchedSections = parseSummary(patched).sections;
|
|
9825
|
+
const patchedBodies = new Map(patchedSections.map((section) => [
|
|
9826
|
+
sectionIdentity(section),
|
|
9827
|
+
section.body.trim()
|
|
9828
|
+
]));
|
|
9829
|
+
const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
|
|
9830
|
+
return preserved ? patched : summary;
|
|
9831
|
+
} catch (error2) {
|
|
9832
|
+
debug("patchSummary LLM failed", error2);
|
|
9833
|
+
return summary;
|
|
9572
9834
|
}
|
|
9573
|
-
const constraints = uniqueByText([
|
|
9574
|
-
...extraction.constraints.flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
9575
|
-
...(continuity?.constraints ?? []).flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
9576
|
-
...steeringConstraints
|
|
9577
|
-
], (item) => item.text).filter((item) => !isDiagnosticConstraintText(item.text));
|
|
9578
|
-
const decisions = uniqueByText([
|
|
9579
|
-
...extraction.decisions.flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : []),
|
|
9580
|
-
...(continuity?.decisions ?? []).flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : [])
|
|
9581
|
-
], (item) => item.summary);
|
|
9582
|
-
return {
|
|
9583
|
-
unresolved,
|
|
9584
|
-
resolved,
|
|
9585
|
-
constraints,
|
|
9586
|
-
decisions,
|
|
9587
|
-
goal: extraction.mainGoal ?? continuity?.goal ?? null
|
|
9588
|
-
};
|
|
9589
9835
|
}
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
}
|
|
9836
|
+
|
|
9837
|
+
// src/domain/yield-gate.ts
|
|
9838
|
+
class YieldGateError extends Error {
|
|
9839
|
+
reason;
|
|
9840
|
+
name = "YieldGateError";
|
|
9841
|
+
constructor(reason, estimate) {
|
|
9842
|
+
super(reason === "target-miss" ? "Final summary estimate misses the planned compaction target" : "Final summary estimate does not meet the minimum saving policy");
|
|
9843
|
+
this.reason = reason;
|
|
9844
|
+
Object.assign(this, estimate);
|
|
9600
9845
|
}
|
|
9846
|
+
plannedAfterTokens;
|
|
9847
|
+
plannedSavedTokens;
|
|
9848
|
+
plannedYield;
|
|
9849
|
+
estimatedAfterTokens;
|
|
9850
|
+
estimatedSavedTokens;
|
|
9851
|
+
estimatedYield;
|
|
9852
|
+
retainedTailTokens;
|
|
9853
|
+
summaryTokens;
|
|
9854
|
+
summaryBudgetTokens;
|
|
9855
|
+
targetAfterTokens;
|
|
9856
|
+
relaxedSoftBoundaries;
|
|
9857
|
+
hardBoundaryAdjusted;
|
|
9601
9858
|
}
|
|
9602
|
-
function
|
|
9603
|
-
const
|
|
9604
|
-
const
|
|
9605
|
-
const
|
|
9606
|
-
const
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
const ownerCounts = new Map(Array.from(ownerSets, ([file, owners]) => [file, owners.size]));
|
|
9623
|
-
const listed = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expected);
|
|
9624
|
-
const modifiedListed = listed("files-modified");
|
|
9625
|
-
const readListed = listed("files-read");
|
|
9626
|
-
const deletedListed = listed("files-deleted");
|
|
9627
|
-
for (const file of modified) {
|
|
9628
|
-
const display = rendered.get(file);
|
|
9629
|
-
if (display && !hasListedPath(modifiedListed, file, display, ownerCounts)) {
|
|
9630
|
-
addGap(accumulator, { kind: "missing-file", path: file }, 5);
|
|
9631
|
-
}
|
|
9859
|
+
function verifyCompactionYield(totalTokens, summaryTokens, plan) {
|
|
9860
|
+
const estimatedAfterTokens = plan.fixedContextTokens + plan.retainedTokens + summaryTokens;
|
|
9861
|
+
const estimatedSavedTokens = Math.max(0, totalTokens - estimatedAfterTokens);
|
|
9862
|
+
const estimatedYield = totalTokens > 0 ? estimatedSavedTokens / totalTokens : 0;
|
|
9863
|
+
const estimate = {
|
|
9864
|
+
plannedAfterTokens: plan.projectedAfterTokens,
|
|
9865
|
+
plannedSavedTokens: plan.projectedSavedTokens,
|
|
9866
|
+
plannedYield: plan.projectedYield,
|
|
9867
|
+
estimatedAfterTokens,
|
|
9868
|
+
estimatedSavedTokens,
|
|
9869
|
+
estimatedYield,
|
|
9870
|
+
retainedTailTokens: plan.retainedTokens,
|
|
9871
|
+
summaryTokens,
|
|
9872
|
+
summaryBudgetTokens: plan.summaryBudgetTokens,
|
|
9873
|
+
targetAfterTokens: plan.targetAfterTokens,
|
|
9874
|
+
relaxedSoftBoundaries: plan.relaxedSoftBoundaries,
|
|
9875
|
+
hardBoundaryAdjusted: plan.hardBoundaryAdjusted
|
|
9876
|
+
};
|
|
9877
|
+
if (estimatedAfterTokens > plan.targetAfterTokens + ESTIMATOR_ROUNDING_TOLERANCE_TOKENS) {
|
|
9878
|
+
throw new YieldGateError("target-miss", estimate);
|
|
9632
9879
|
}
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
if (display && !hasListedPath(readListed, file, display, ownerCounts)) {
|
|
9636
|
-
addGap(accumulator, { kind: "missing-read-file", path: file }, 5);
|
|
9637
|
-
}
|
|
9880
|
+
if (estimatedYield < MIN_COMPACTION_SAVING_RATIO) {
|
|
9881
|
+
throw new YieldGateError("insufficient-saving", estimate);
|
|
9638
9882
|
}
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9883
|
+
return estimate;
|
|
9884
|
+
}
|
|
9885
|
+
|
|
9886
|
+
// src/ui/error-format.ts
|
|
9887
|
+
function failureAction(kind) {
|
|
9888
|
+
switch (kind) {
|
|
9889
|
+
case "authentication":
|
|
9890
|
+
return "Check the selected model's credentials or use /login.";
|
|
9891
|
+
case "rate-limit":
|
|
9892
|
+
return "Wait for the provider quota to recover before retrying.";
|
|
9893
|
+
case "timeout":
|
|
9894
|
+
return "Try Fast or review the latency budget in /smart-compact settings.";
|
|
9895
|
+
case "budget":
|
|
9896
|
+
return "Review call/token limits in /smart-compact settings.";
|
|
9897
|
+
case "output-limit":
|
|
9898
|
+
return "Review the model output limit and reasoning level.";
|
|
9899
|
+
case "provider":
|
|
9900
|
+
case "validation":
|
|
9901
|
+
return "Check model availability and /smart-compact metrics; select another route manually if needed.";
|
|
9902
|
+
case "cancelled":
|
|
9903
|
+
return "Retry when ready.";
|
|
9904
|
+
default:
|
|
9905
|
+
return "For local stack diagnostics, restart Pi with DEBUG=smart-compact and reproduce.";
|
|
9644
9906
|
}
|
|
9645
|
-
return { modified, read, deleted, rendered };
|
|
9646
9907
|
}
|
|
9647
|
-
function
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9908
|
+
function formatGenerationFailureForUi(error2) {
|
|
9909
|
+
const kind = classifyTelemetryFailure(error2);
|
|
9910
|
+
return kind + ". " + failureAction(kind);
|
|
9911
|
+
}
|
|
9912
|
+
function formatCompactErrorForUi(error2) {
|
|
9913
|
+
if (error2 instanceof VerificationGateError) {
|
|
9914
|
+
const kinds = error2.gapKinds.slice(0, 4).join(", ") || "unknown";
|
|
9915
|
+
return "Verification stopped apply at the " + error2.stage + " gate: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. Conversation unchanged. " + "Review /smart-compact metrics; do not bypass verification. For local evidence, restart Pi with DEBUG=smart-compact.";
|
|
9653
9916
|
}
|
|
9654
|
-
|
|
9655
|
-
const
|
|
9656
|
-
|
|
9657
|
-
addGap(accumulator, { kind: "missing-error", message: error2.message, resolved: true }, 2);
|
|
9658
|
-
}
|
|
9917
|
+
if (error2 instanceof YieldGateError) {
|
|
9918
|
+
const reason = error2.reason === "target-miss" ? "target missed" : "saving below 10%";
|
|
9919
|
+
return "Yield check stopped apply: estimated " + error2.estimatedAfterTokens.toLocaleString() + "t after vs " + error2.targetAfterTokens.toLocaleString() + "t target (" + reason + "). Conversation unchanged. Try /smart-compact balanced for a larger target; safety checks still apply.";
|
|
9659
9920
|
}
|
|
9921
|
+
return "Smart compact failed [" + classifyTelemetryFailure(error2) + "]. Conversation unchanged. " + failureAction(classifyTelemetryFailure(error2));
|
|
9660
9922
|
}
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
}, 20);
|
|
9676
|
-
}
|
|
9677
|
-
}
|
|
9678
|
-
if (collected.goal) {
|
|
9679
|
-
const goalTarget = findSection(parsed, "goal")?.body ?? "";
|
|
9680
|
-
if (!hasSemanticEvidence(collected.goal, goalTarget)) {
|
|
9681
|
-
addGap(accumulator, { kind: "missing-goal", goal: collected.goal }, 12);
|
|
9682
|
-
}
|
|
9683
|
-
if (hasSemanticContradiction(collected.goal, goalTarget)) {
|
|
9684
|
-
addGap(accumulator, {
|
|
9685
|
-
kind: "inconsistency",
|
|
9686
|
-
detail: "semantic-contradiction: goal polarity or condition changed"
|
|
9687
|
-
}, 20);
|
|
9923
|
+
|
|
9924
|
+
// src/app/steps/synthesize.ts
|
|
9925
|
+
async function summarizeConversation(rc) {
|
|
9926
|
+
let synthPhaseStart = Date.now();
|
|
9927
|
+
const extraction = rc.extraction;
|
|
9928
|
+
rc.mode ??= rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced";
|
|
9929
|
+
rc.requestedMode ??= rc.mode;
|
|
9930
|
+
if (rc.requestedMode === "auto") {
|
|
9931
|
+
const refined = resolveMode("auto", rc.contextPercent, extraction, continuityRisk(rc.previousState) + (rc.adapted ? 12 : 0));
|
|
9932
|
+
if (refined !== rc.mode) {
|
|
9933
|
+
rc.mode = refined;
|
|
9934
|
+
const policy2 = MODE_POLICIES[refined];
|
|
9935
|
+
rc.services.budget.setLimits(resolveCallBudget(rc.config.maxLlmCalls, refined, rc.maxLlmCalls, rc.flags.autoTriggered && !rc.flags.skipCompact), effectiveBudget(rc.config.maxLlmInputTokens, policy2.maxInputTokens, rc.maxLlmInputTokens), policy2.maxOutputTokens);
|
|
9936
|
+
rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
|
|
9688
9937
|
}
|
|
9689
9938
|
}
|
|
9690
|
-
const
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
}
|
|
9939
|
+
const pc = rc.profileCfg;
|
|
9940
|
+
const policy = MODE_POLICIES[rc.mode];
|
|
9941
|
+
const cacheKey = synthesisCacheKey(rc);
|
|
9942
|
+
const cached = getCachedSynthesis(cacheKey);
|
|
9943
|
+
if (cached) {
|
|
9944
|
+
rc.notify("Synthesis cache hit \u2014 no LLM calls", "info");
|
|
9945
|
+
showProgressOverlay(rc.ctx, {
|
|
9946
|
+
phase: 3,
|
|
9947
|
+
phaseName: "Synthesize",
|
|
9948
|
+
detail: "Reusing the cached continuation summary \xB7 no LLM call"
|
|
9949
|
+
});
|
|
9950
|
+
Object.assign(rc, {
|
|
9951
|
+
finalSummary: cached.finalSummary,
|
|
9952
|
+
method: cached.method,
|
|
9953
|
+
methodForMetrics: cached.method + "-cache",
|
|
9954
|
+
generationFallbacks: [],
|
|
9955
|
+
llmCalls: 0,
|
|
9956
|
+
summaries: cached.summaries,
|
|
9957
|
+
explorationReport: cached.explorationReport,
|
|
9958
|
+
explorationRounds: cached.explorationRounds,
|
|
9959
|
+
chunkCount: cached.chunkCount
|
|
9960
|
+
});
|
|
9961
|
+
const hit = advance(rc, "_synthesized");
|
|
9962
|
+
markMeasuredPhase(hit, "synthesize", synthPhaseStart);
|
|
9963
|
+
return hit;
|
|
9701
9964
|
}
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
...continuity?.readFiles ?? [],
|
|
9737
|
-
...(continuity?.unresolvedErrors ?? []).flatMap((error2) => error2.files),
|
|
9738
|
-
...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
|
|
9739
|
-
]));
|
|
9740
|
-
for (const ref of new Set(extractFileRefs(summary))) {
|
|
9741
|
-
const grounded = isKnownPathReference(ref, knownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
|
|
9742
|
-
if (!grounded)
|
|
9743
|
-
addGap(accumulator, { kind: "fabricated-file", ref }, 4);
|
|
9965
|
+
const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
|
|
9966
|
+
conversationTokens: rc.convTokens,
|
|
9967
|
+
toolPercent: rc.toolPercent
|
|
9968
|
+
}) >= 0.85;
|
|
9969
|
+
if (zeroCall) {
|
|
9970
|
+
showProgressOverlay(rc.ctx, {
|
|
9971
|
+
phase: 3,
|
|
9972
|
+
phaseName: "Synthesize",
|
|
9973
|
+
detail: "Building a deterministic continuation summary \xB7 no LLM call"
|
|
9974
|
+
});
|
|
9975
|
+
const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
9976
|
+
setCachedSynthesis(cacheKey, {
|
|
9977
|
+
finalSummary: finalSummary2,
|
|
9978
|
+
method: "heuristic",
|
|
9979
|
+
summaries: [],
|
|
9980
|
+
explorationReport: null,
|
|
9981
|
+
explorationRounds: 0,
|
|
9982
|
+
chunkCount: 0
|
|
9983
|
+
});
|
|
9984
|
+
rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
|
|
9985
|
+
Object.assign(rc, {
|
|
9986
|
+
finalSummary: finalSummary2,
|
|
9987
|
+
method: "heuristic",
|
|
9988
|
+
methodForMetrics: "zero-call",
|
|
9989
|
+
generationFallbacks: [],
|
|
9990
|
+
llmCalls: 0,
|
|
9991
|
+
summaries: [],
|
|
9992
|
+
explorationReport: null,
|
|
9993
|
+
explorationRounds: 0,
|
|
9994
|
+
chunkCount: 0
|
|
9995
|
+
});
|
|
9996
|
+
const deterministic = advance(rc, "_synthesized");
|
|
9997
|
+
markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
|
|
9998
|
+
return deterministic;
|
|
9744
9999
|
}
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
const
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
const
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
10000
|
+
const shouldSkipExplore = !policy.explore;
|
|
10001
|
+
const convText = rc.convText;
|
|
10002
|
+
const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * rc.providerCaps.singlePassTokenMultiplier * policy.singlePassMultiplier);
|
|
10003
|
+
rc.vlog("Tier=" + rc.tier + " | convTokens=" + rc.convTokens + " | singlePassMax=" + singlePassMaxTokens);
|
|
10004
|
+
let finalSummary;
|
|
10005
|
+
let method;
|
|
10006
|
+
const summaries = [];
|
|
10007
|
+
let explorationReport = null;
|
|
10008
|
+
let explorationRounds = 0;
|
|
10009
|
+
let chunkCount = 0;
|
|
10010
|
+
let cacheable = true;
|
|
10011
|
+
const generationFallbacks = [];
|
|
10012
|
+
let summaryAuth;
|
|
10013
|
+
try {
|
|
10014
|
+
summaryAuth = await resolveStageAuth(rc, "summary");
|
|
10015
|
+
} catch (error2) {
|
|
10016
|
+
cacheable = false;
|
|
10017
|
+
generationFallbacks.push("summary route unavailable");
|
|
10018
|
+
debugError("Summary route unavailable", error2);
|
|
10019
|
+
rc.notify("Summary route unavailable \xB7 using deterministic fallback [" + formatGenerationFailureForUi(error2) + "]", "warning");
|
|
9757
10020
|
}
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
const unresolved = collected.unresolved.find((error2) => {
|
|
9764
|
-
const firstLine = error2.message.split(/\r?\n/, 1)[0] ?? "";
|
|
9765
|
-
const refs = extractFileRefs(firstLine).map(normalizePath);
|
|
9766
|
-
return needles.some((needle) => refs.includes(normalizePath(needle)));
|
|
10021
|
+
if (!summaryAuth) {
|
|
10022
|
+
showProgressOverlay(rc.ctx, {
|
|
10023
|
+
phase: 3,
|
|
10024
|
+
phaseName: "Synthesize",
|
|
10025
|
+
detail: "Summary route unavailable \xB7 building a deterministic summary"
|
|
9767
10026
|
});
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
10027
|
+
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
10028
|
+
method = "heuristic";
|
|
10029
|
+
} else if (rc.convTokens < singlePassMaxTokens) {
|
|
10030
|
+
showProgressOverlay(rc.ctx, {
|
|
10031
|
+
phase: 3,
|
|
10032
|
+
phaseName: "Synthesize",
|
|
10033
|
+
detail: "Writing one continuation summary from " + rc.convTokens.toLocaleString() + " tokens",
|
|
10034
|
+
model: rc.modelLabel,
|
|
10035
|
+
profile: rc.profile,
|
|
10036
|
+
extraction
|
|
10037
|
+
});
|
|
10038
|
+
try {
|
|
10039
|
+
const r = await singlePassCompact(convText, extraction, null, rc.prevContext + rc.projectCtx, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined);
|
|
10040
|
+
finalSummary = r.summary;
|
|
10041
|
+
method = "single-pass";
|
|
10042
|
+
} catch (err) {
|
|
10043
|
+
cacheable = false;
|
|
10044
|
+
generationFallbacks.push("single-pass generation failed");
|
|
10045
|
+
debugError("Single-pass synthesis used deterministic fallback", err);
|
|
10046
|
+
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "warning");
|
|
10047
|
+
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
10048
|
+
method = "heuristic";
|
|
9773
10049
|
}
|
|
9774
|
-
}
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
10050
|
+
} else {
|
|
10051
|
+
const needsExploration = !shouldSkipExplore && shouldExplore(extraction);
|
|
10052
|
+
if (needsExploration) {
|
|
10053
|
+
const exploreStart = Date.now();
|
|
10054
|
+
showProgressOverlay(rc.ctx, {
|
|
10055
|
+
phase: 2,
|
|
10056
|
+
phaseName: "Explore",
|
|
10057
|
+
detail: "Mapping topic shifts and continuity risks",
|
|
10058
|
+
model: rc.modelLabel,
|
|
10059
|
+
profile: rc.profile,
|
|
10060
|
+
extraction
|
|
10061
|
+
});
|
|
10062
|
+
try {
|
|
10063
|
+
const segAuth = await resolveStageAuth(rc, "explore");
|
|
10064
|
+
const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [
|
|
10065
|
+
rc.userNote,
|
|
10066
|
+
rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined
|
|
10067
|
+
].filter(Boolean).join(`
|
|
10068
|
+
`) || undefined, rc.cancellation.signal, MAX_EXPLORATION_ROUNDS, rc.notify, rc.services);
|
|
10069
|
+
explorationReport = expResult.report;
|
|
10070
|
+
explorationRounds = expResult.rounds;
|
|
10071
|
+
rc.notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
10072
|
+
rc.vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
|
|
10073
|
+
} catch (err) {
|
|
10074
|
+
cacheable = false;
|
|
10075
|
+
generationFallbacks.push("exploration unavailable");
|
|
10076
|
+
debugError("Explore used deterministic topic boundaries", err);
|
|
10077
|
+
rc.notify("Explore unavailable \xB7 using deterministic topic boundaries", "info");
|
|
10078
|
+
} finally {
|
|
10079
|
+
const exploreEnd = Date.now();
|
|
10080
|
+
markMeasuredPhase(rc, "explore", exploreStart, exploreEnd);
|
|
10081
|
+
synthPhaseStart = exploreEnd;
|
|
10082
|
+
}
|
|
10083
|
+
} else {
|
|
10084
|
+
rc.notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
|
|
9787
10085
|
}
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
ok: accumulator.gaps.length === 0 && score >= 85,
|
|
9804
|
-
gaps: accumulator.gaps,
|
|
9805
|
-
score
|
|
9806
|
-
};
|
|
9807
|
-
}
|
|
9808
|
-
function patchDeterministic(summary, gaps, extraction, continuity = null, evidence = {}) {
|
|
9809
|
-
let canonical = parseSummary(summary);
|
|
9810
|
-
const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
|
|
9811
|
-
const readPaths = extraction.readFiles;
|
|
9812
|
-
const deletedPaths = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
|
|
9813
|
-
const pathEvidence = buildSummaryPathEvidence([...modifiedPaths, ...readPaths, ...deletedPaths], evidence.summaryBudgetTokens);
|
|
9814
|
-
const replaceFileSection = (kind, paths) => {
|
|
9815
|
-
const body = paths.map((path12) => "- " + (pathEvidence.get(path12) ?? JSON.stringify(path12))).join(`
|
|
9816
|
-
`);
|
|
9817
|
-
canonical = upsertSection(canonical, kind, body || "- None recorded.");
|
|
9818
|
-
};
|
|
9819
|
-
const safe = (value, max = TRUNC.MESSAGE) => summaryEvidenceLine(value, max);
|
|
9820
|
-
const unresolvedMessages = Array.from(new Set([
|
|
9821
|
-
...extraction.errors.filter((error2) => !error2.resolved).map((error2) => error2.message),
|
|
9822
|
-
...(continuity?.unresolvedErrors ?? []).map((error2) => error2.message)
|
|
9823
|
-
]));
|
|
9824
|
-
const unresolvedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved");
|
|
9825
|
-
const blockedItems = [
|
|
9826
|
-
...unresolvedMessages.map((message) => safe(message)).filter(Boolean).map((message) => "- " + message),
|
|
9827
|
-
...unresolvedLoops.map((loop) => safe(loop.summary)).filter(Boolean).map((summary2) => "- " + summary2)
|
|
9828
|
-
];
|
|
9829
|
-
const patchBlockedNone = () => {
|
|
9830
|
-
const progress = findSection(canonical, "progress");
|
|
9831
|
-
if (!progress || !blockedItems.length)
|
|
9832
|
-
return;
|
|
9833
|
-
const lines = progress.body.split(/\r?\n/);
|
|
9834
|
-
const start = lines.findIndex((line) => /^###\s*Blocked\s*$/i.test(line.trim()));
|
|
9835
|
-
if (start < 0)
|
|
9836
|
-
return;
|
|
9837
|
-
let end = lines.findIndex((line, index) => index > start && /^###\s+/.test(line.trim()));
|
|
9838
|
-
if (end < 0)
|
|
9839
|
-
end = lines.length;
|
|
9840
|
-
const existing = lines.slice(start + 1, end);
|
|
9841
|
-
const noneIndexes = noneBlockerLineIndexes(existing);
|
|
9842
|
-
if (!noneIndexes.size)
|
|
9843
|
-
return;
|
|
9844
|
-
const replacement = Array.from(new Set([
|
|
9845
|
-
...blockedItems,
|
|
9846
|
-
...existing.filter((line, index) => line.trim() && !noneIndexes.has(index))
|
|
9847
|
-
]));
|
|
9848
|
-
lines.splice(start + 1, end - start - 1, ...replacement);
|
|
9849
|
-
canonical = upsertSection(canonical, "progress", lines.join(`
|
|
9850
|
-
`));
|
|
9851
|
-
};
|
|
9852
|
-
for (const gap of gaps) {
|
|
9853
|
-
switch (gap.kind) {
|
|
9854
|
-
case "missing-section": {
|
|
9855
|
-
if (gap.section === "goal") {
|
|
9856
|
-
canonical = upsertSection(canonical, "goal", safe(extraction.mainGoal ?? "", TRUNC.DETAIL) || "Continue the current coding task.");
|
|
9857
|
-
} else if (gap.section === "progress") {
|
|
9858
|
-
canonical = upsertSection(canonical, "progress", `### Done
|
|
9859
|
-
- No explicit completion recorded.
|
|
9860
|
-
### In Progress
|
|
9861
|
-
- Continue from the latest user request.
|
|
9862
|
-
### Blocked
|
|
9863
|
-
` + (blockedItems.join(`
|
|
9864
|
-
`) || "- None recorded."));
|
|
9865
|
-
} else if (gap.section === "critical-context") {
|
|
9866
|
-
const critical = unresolvedMessages.flatMap((message) => {
|
|
9867
|
-
const text = safe(message);
|
|
9868
|
-
return text ? ["- Unresolved error: " + text] : [];
|
|
9869
|
-
});
|
|
9870
|
-
canonical = upsertSection(canonical, "critical-context", critical.join(`
|
|
9871
|
-
`) || "- None recorded.");
|
|
10086
|
+
let boundaries;
|
|
10087
|
+
if (explorationReport?.boundaries.length) {
|
|
10088
|
+
const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
|
|
10089
|
+
const heuristicBounds = extraction.topics.map((t) => ({
|
|
10090
|
+
afterIndex: t.endIndex,
|
|
10091
|
+
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
10092
|
+
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
10093
|
+
confidence: 0.6
|
|
10094
|
+
}));
|
|
10095
|
+
if (llmBounds.length > 0) {
|
|
10096
|
+
const merged = [...llmBounds];
|
|
10097
|
+
for (const hb of heuristicBounds) {
|
|
10098
|
+
const nearby = merged.find((m) => Math.abs(m.afterIndex - hb.afterIndex) <= 3);
|
|
10099
|
+
if (!nearby)
|
|
10100
|
+
merged.push(hb);
|
|
9872
10101
|
}
|
|
9873
|
-
|
|
10102
|
+
boundaries = merged.sort((a, b) => a.afterIndex - b.afterIndex);
|
|
10103
|
+
} else {
|
|
10104
|
+
boundaries = heuristicBounds;
|
|
9874
10105
|
}
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
10106
|
+
} else {
|
|
10107
|
+
boundaries = extraction.topics.map((t) => ({
|
|
10108
|
+
afterIndex: t.endIndex,
|
|
10109
|
+
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
10110
|
+
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
10111
|
+
confidence: 0.6
|
|
10112
|
+
}));
|
|
10113
|
+
}
|
|
10114
|
+
const chunks = chunkLlmMessages(rc.llmMessages, boundaries, pc, rc.estimator, rc.config.focusWeighting ? rc.focus : undefined);
|
|
10115
|
+
chunkCount = chunks.length;
|
|
10116
|
+
rc.notify("Chunked: " + chunkCount + " chunks", "info");
|
|
10117
|
+
rc.vlog("Chunk topics: " + chunks.map((c) => c.topic + "[" + c.startIndex + "-" + c.endIndex + "]").join(", "));
|
|
10118
|
+
const batches = createBatches(chunks, pc.batchMaxTokens);
|
|
10119
|
+
const totalBatches = batches.length;
|
|
10120
|
+
showProgressOverlay(rc.ctx, {
|
|
10121
|
+
phase: 3,
|
|
10122
|
+
phaseName: "Synthesize",
|
|
10123
|
+
detail: "Compressing older history \xB7 batch 0/" + totalBatches,
|
|
10124
|
+
model: rc.modelLabel,
|
|
10125
|
+
profile: rc.profile,
|
|
10126
|
+
extraction,
|
|
10127
|
+
explorationRounds,
|
|
10128
|
+
totalBatches
|
|
10129
|
+
});
|
|
10130
|
+
const concurrency = rc.providerCaps.concurrencyLimit;
|
|
10131
|
+
if (totalBatches <= 1) {
|
|
10132
|
+
const single = batches[0];
|
|
10133
|
+
if (single) {
|
|
10134
|
+
if (rc.services.budget.remainingCalls() <= 1) {
|
|
10135
|
+
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
10136
|
+
cacheable = false;
|
|
10137
|
+
generationFallbacks.push("call budget reserved for final assembly");
|
|
10138
|
+
rc.notify("Call budget: chunk synthesis uses deterministic evidence so final assembly remains available", "info");
|
|
10139
|
+
} else {
|
|
10140
|
+
try {
|
|
10141
|
+
summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, single.length, rc.providerCaps.maxOutputTokens), rc.sessionId));
|
|
10142
|
+
} catch (err) {
|
|
10143
|
+
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
10144
|
+
cacheable = false;
|
|
10145
|
+
generationFallbacks.push("1 synthesis batch fallback");
|
|
10146
|
+
debugError("Synthesis batch used deterministic fallback", err);
|
|
10147
|
+
rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(err) + "]", "warning");
|
|
10148
|
+
showProgressOverlay(rc.ctx, {
|
|
10149
|
+
phase: 3,
|
|
10150
|
+
phaseName: "Synthesize",
|
|
10151
|
+
detail: "1 batch fallback \xB7 preserving coverage from deterministic evidence",
|
|
10152
|
+
explorationRounds
|
|
10153
|
+
});
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
} else {
|
|
10157
|
+
rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
|
|
10158
|
+
}
|
|
10159
|
+
} else {
|
|
10160
|
+
const results = new Array(totalBatches);
|
|
10161
|
+
const errors = new Array(totalBatches).fill(null);
|
|
10162
|
+
const batchCallLimit = Math.max(0, Math.min(totalBatches, rc.services.budget.remainingCalls() - 1));
|
|
10163
|
+
for (let index = batchCallLimit;index < totalBatches; index++) {
|
|
10164
|
+
results[index] = batches[index].map((chunk) => failedChunkSummary(chunk));
|
|
10165
|
+
}
|
|
10166
|
+
if (batchCallLimit < totalBatches) {
|
|
10167
|
+
rc.notify("Call budget: " + (totalBatches - batchCallLimit) + " batch(es) use deterministic fallback to reserve assembly", "info");
|
|
10168
|
+
cacheable = false;
|
|
10169
|
+
generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
|
|
10170
|
+
}
|
|
10171
|
+
let completed = totalBatches - batchCallLimit;
|
|
10172
|
+
let nextBatch = 0;
|
|
10173
|
+
let budgetStopped = false;
|
|
10174
|
+
const runWorker = async () => {
|
|
10175
|
+
while (true) {
|
|
10176
|
+
const idx = nextBatch++;
|
|
10177
|
+
if (idx >= batchCallLimit)
|
|
10178
|
+
return;
|
|
10179
|
+
if (budgetStopped || rc.services.budget.reason()) {
|
|
10180
|
+
budgetStopped = true;
|
|
10181
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
10182
|
+
} else {
|
|
10183
|
+
try {
|
|
10184
|
+
const batch = batches[idx];
|
|
10185
|
+
results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
|
|
10186
|
+
} catch (err) {
|
|
10187
|
+
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
10188
|
+
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
completed++;
|
|
10192
|
+
showProgressOverlay(rc.ctx, {
|
|
10193
|
+
phase: 3,
|
|
10194
|
+
phaseName: "Synthesize",
|
|
10195
|
+
detail: "Compressing older history \xB7 batch " + completed + "/" + totalBatches,
|
|
10196
|
+
model: rc.modelLabel,
|
|
10197
|
+
profile: rc.profile,
|
|
10198
|
+
extraction,
|
|
10199
|
+
explorationRounds,
|
|
10200
|
+
totalBatches,
|
|
10201
|
+
currentBatch: completed
|
|
10202
|
+
});
|
|
9889
10203
|
}
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
break;
|
|
9898
|
-
case "missing-goal":
|
|
9899
|
-
canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
|
|
9900
|
-
break;
|
|
9901
|
-
case "missing-open-loops": {
|
|
9902
|
-
const current = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
|
|
9903
|
-
const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
|
|
9904
|
-
const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({
|
|
9905
|
-
priority: loop.priority,
|
|
9906
|
-
summary: safe(loop.summary, TRUNC.SNIPPET)
|
|
9907
|
-
})).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
|
|
9908
|
-
const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
|
|
9909
|
-
`);
|
|
9910
|
-
canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
|
|
9911
|
-
break;
|
|
10204
|
+
};
|
|
10205
|
+
const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
|
|
10206
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
10207
|
+
if (budgetStopped) {
|
|
10208
|
+
rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
|
|
10209
|
+
cacheable = false;
|
|
10210
|
+
generationFallbacks.push("synthesis budget exhausted during batch pool");
|
|
9912
10211
|
}
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
10212
|
+
for (const r of results)
|
|
10213
|
+
if (r)
|
|
10214
|
+
summaries.push(...r);
|
|
10215
|
+
const failedBatches = errors.filter(Boolean);
|
|
10216
|
+
for (const error2 of failedBatches)
|
|
10217
|
+
debugError("Synthesis batch used deterministic fallback", error2);
|
|
10218
|
+
if (failedBatches.length) {
|
|
10219
|
+
cacheable = false;
|
|
10220
|
+
generationFallbacks.push(failedBatches.length + " synthesis batch fallback(s)");
|
|
10221
|
+
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(failedBatches[0]) + "]", "warning");
|
|
10222
|
+
showProgressOverlay(rc.ctx, {
|
|
10223
|
+
phase: 3,
|
|
10224
|
+
phaseName: "Synthesize",
|
|
10225
|
+
detail: failedBatches.length + " batch fallback(s) \xB7 preserving coverage from deterministic evidence",
|
|
10226
|
+
explorationRounds
|
|
10227
|
+
});
|
|
9929
10228
|
}
|
|
9930
|
-
case "unsupported-claim":
|
|
9931
|
-
canonical = removeUnsupportedClaim(canonical, gap.claim);
|
|
9932
|
-
break;
|
|
9933
|
-
case "inconsistency":
|
|
9934
|
-
if (gap.detail.startsWith("blocked-none:"))
|
|
9935
|
-
patchBlockedNone();
|
|
9936
|
-
break;
|
|
9937
10229
|
}
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
10230
|
+
showProgressOverlay(rc.ctx, {
|
|
10231
|
+
phase: 3,
|
|
10232
|
+
phaseName: "Synthesize",
|
|
10233
|
+
detail: "Merging summaries with project continuity",
|
|
10234
|
+
model: rc.modelLabel,
|
|
10235
|
+
profile: rc.profile,
|
|
10236
|
+
extraction,
|
|
10237
|
+
explorationRounds,
|
|
10238
|
+
totalBatches: batches.length
|
|
10239
|
+
});
|
|
10240
|
+
method = "eesv";
|
|
10241
|
+
try {
|
|
10242
|
+
const r = await assembleLLM(summaries, extraction, explorationReport, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.prevContext, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined, rc.previousState);
|
|
10243
|
+
if (r?.startsWith("##"))
|
|
10244
|
+
finalSummary = r;
|
|
10245
|
+
else
|
|
10246
|
+
throw new Error("Invalid summary response");
|
|
10247
|
+
} catch (err) {
|
|
10248
|
+
cacheable = false;
|
|
10249
|
+
generationFallbacks.push("assembly generation failed");
|
|
10250
|
+
debugError("Assembly used deterministic fallback", err);
|
|
10251
|
+
rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "warning");
|
|
10252
|
+
method = "heuristic";
|
|
10253
|
+
finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
9952
10254
|
}
|
|
9953
10255
|
}
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
|
|
9976
|
-
const response = await trackedComplete("patch", model, {
|
|
9977
|
-
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
9978
|
-
messages: [
|
|
9979
|
-
{
|
|
9980
|
-
role: "user",
|
|
9981
|
-
content: [{ type: "text", text: patchPrompt }],
|
|
9982
|
-
timestamp: Date.now()
|
|
9983
|
-
}
|
|
9984
|
-
]
|
|
9985
|
-
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
|
|
9986
|
-
const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
|
|
9987
|
-
`).trim();
|
|
9988
|
-
if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
|
|
9989
|
-
return summary;
|
|
9990
|
-
const originalSections = parseSummary(summary).sections;
|
|
9991
|
-
const patchedSections = parseSummary(patched).sections;
|
|
9992
|
-
const patchedBodies = new Map(patchedSections.map((section) => [
|
|
9993
|
-
sectionIdentity(section),
|
|
9994
|
-
section.body.trim()
|
|
9995
|
-
]));
|
|
9996
|
-
const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
|
|
9997
|
-
return preserved ? patched : summary;
|
|
9998
|
-
} catch (error2) {
|
|
9999
|
-
debug("patchSummary LLM failed", error2);
|
|
10000
|
-
return summary;
|
|
10256
|
+
Object.assign(rc, {
|
|
10257
|
+
finalSummary,
|
|
10258
|
+
method,
|
|
10259
|
+
methodForMetrics: method,
|
|
10260
|
+
generationFallbacks,
|
|
10261
|
+
llmCalls: rc.services.metrics.summary().totalCalls,
|
|
10262
|
+
summaries,
|
|
10263
|
+
explorationReport,
|
|
10264
|
+
explorationRounds,
|
|
10265
|
+
chunkCount
|
|
10266
|
+
});
|
|
10267
|
+
const out = advance(rc, "_synthesized");
|
|
10268
|
+
if (cacheable) {
|
|
10269
|
+
setCachedSynthesis(cacheKey, {
|
|
10270
|
+
finalSummary,
|
|
10271
|
+
method,
|
|
10272
|
+
summaries,
|
|
10273
|
+
explorationReport,
|
|
10274
|
+
explorationRounds,
|
|
10275
|
+
chunkCount
|
|
10276
|
+
});
|
|
10001
10277
|
}
|
|
10278
|
+
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
10279
|
+
return out;
|
|
10002
10280
|
}
|
|
10003
10281
|
|
|
10004
10282
|
// src/app/steps/verify.ts
|
|
@@ -10114,57 +10392,6 @@ async function verifyAndPatch(rc) {
|
|
|
10114
10392
|
// src/app/steps/state.ts
|
|
10115
10393
|
import fs8 from "fs";
|
|
10116
10394
|
import path12 from "path";
|
|
10117
|
-
|
|
10118
|
-
// src/domain/yield-gate.ts
|
|
10119
|
-
class YieldGateError extends Error {
|
|
10120
|
-
reason;
|
|
10121
|
-
name = "YieldGateError";
|
|
10122
|
-
constructor(reason, estimate) {
|
|
10123
|
-
super(reason === "target-miss" ? "Final summary estimate misses the planned compaction target" : "Final summary estimate does not meet the minimum saving policy");
|
|
10124
|
-
this.reason = reason;
|
|
10125
|
-
Object.assign(this, estimate);
|
|
10126
|
-
}
|
|
10127
|
-
plannedAfterTokens;
|
|
10128
|
-
plannedSavedTokens;
|
|
10129
|
-
plannedYield;
|
|
10130
|
-
estimatedAfterTokens;
|
|
10131
|
-
estimatedSavedTokens;
|
|
10132
|
-
estimatedYield;
|
|
10133
|
-
retainedTailTokens;
|
|
10134
|
-
summaryTokens;
|
|
10135
|
-
summaryBudgetTokens;
|
|
10136
|
-
targetAfterTokens;
|
|
10137
|
-
relaxedSoftBoundaries;
|
|
10138
|
-
hardBoundaryAdjusted;
|
|
10139
|
-
}
|
|
10140
|
-
function verifyCompactionYield(totalTokens, summaryTokens, plan) {
|
|
10141
|
-
const estimatedAfterTokens = plan.fixedContextTokens + plan.retainedTokens + summaryTokens;
|
|
10142
|
-
const estimatedSavedTokens = Math.max(0, totalTokens - estimatedAfterTokens);
|
|
10143
|
-
const estimatedYield = totalTokens > 0 ? estimatedSavedTokens / totalTokens : 0;
|
|
10144
|
-
const estimate = {
|
|
10145
|
-
plannedAfterTokens: plan.projectedAfterTokens,
|
|
10146
|
-
plannedSavedTokens: plan.projectedSavedTokens,
|
|
10147
|
-
plannedYield: plan.projectedYield,
|
|
10148
|
-
estimatedAfterTokens,
|
|
10149
|
-
estimatedSavedTokens,
|
|
10150
|
-
estimatedYield,
|
|
10151
|
-
retainedTailTokens: plan.retainedTokens,
|
|
10152
|
-
summaryTokens,
|
|
10153
|
-
summaryBudgetTokens: plan.summaryBudgetTokens,
|
|
10154
|
-
targetAfterTokens: plan.targetAfterTokens,
|
|
10155
|
-
relaxedSoftBoundaries: plan.relaxedSoftBoundaries,
|
|
10156
|
-
hardBoundaryAdjusted: plan.hardBoundaryAdjusted
|
|
10157
|
-
};
|
|
10158
|
-
if (estimatedAfterTokens > plan.targetAfterTokens + ESTIMATOR_ROUNDING_TOLERANCE_TOKENS) {
|
|
10159
|
-
throw new YieldGateError("target-miss", estimate);
|
|
10160
|
-
}
|
|
10161
|
-
if (estimatedYield < MIN_COMPACTION_SAVING_RATIO) {
|
|
10162
|
-
throw new YieldGateError("insufficient-saving", estimate);
|
|
10163
|
-
}
|
|
10164
|
-
return estimate;
|
|
10165
|
-
}
|
|
10166
|
-
|
|
10167
|
-
// src/app/steps/state.ts
|
|
10168
10395
|
function buildState(rc) {
|
|
10169
10396
|
const extraction = rc.extraction;
|
|
10170
10397
|
let summary = rc.finalSummary;
|
|
@@ -10226,7 +10453,8 @@ function buildState(rc) {
|
|
|
10226
10453
|
compactionState = rc.services.scrubber.scrubValue(compactionState).value;
|
|
10227
10454
|
const verificationEvidence = {
|
|
10228
10455
|
sourceMessages: rc.llmMessages,
|
|
10229
|
-
steering: { focus: rc.focus, note: rc.userNote }
|
|
10456
|
+
steering: { focus: rc.focus, note: rc.userNote },
|
|
10457
|
+
summaryBudgetTokens: rc.profileCfg?.summaryBudgetTokens ?? 6000
|
|
10230
10458
|
};
|
|
10231
10459
|
let postVerification = verifySummary(summary, extraction, compactionState, verificationEvidence);
|
|
10232
10460
|
const postInitialScore = postVerification.score;
|
|
@@ -11055,11 +11283,14 @@ function aggregateProviderRoutes(metrics) {
|
|
|
11055
11283
|
successes: 0,
|
|
11056
11284
|
latency: 0,
|
|
11057
11285
|
input: 0,
|
|
11058
|
-
output: 0
|
|
11286
|
+
output: 0,
|
|
11287
|
+
failures: {}
|
|
11059
11288
|
};
|
|
11060
11289
|
group.calls++;
|
|
11061
11290
|
if (metric.success)
|
|
11062
11291
|
group.successes++;
|
|
11292
|
+
else if (metric.failureKind)
|
|
11293
|
+
group.failures[metric.failureKind] = (group.failures[metric.failureKind] ?? 0) + 1;
|
|
11063
11294
|
group.latency += Math.max(0, metric.latencyMs);
|
|
11064
11295
|
group.input += Math.max(0, metric.inputTokens) + Math.max(0, metric.cacheHitTokens) + Math.max(0, metric.cacheWriteTokens ?? 0);
|
|
11065
11296
|
group.output += Math.max(0, metric.outputTokens);
|
|
@@ -11071,6 +11302,7 @@ function aggregateProviderRoutes(metrics) {
|
|
|
11071
11302
|
model: group.model,
|
|
11072
11303
|
calls: group.calls,
|
|
11073
11304
|
successes: group.successes,
|
|
11305
|
+
...Object.keys(group.failures).length ? { failures: group.failures } : {},
|
|
11074
11306
|
avgLatencyMs: group.calls ? Math.round(group.latency / group.calls) : 0,
|
|
11075
11307
|
inputTokens: group.input,
|
|
11076
11308
|
outputTokens: group.output
|
|
@@ -11232,28 +11464,7 @@ async function recordFailureMetrics(rc, err, fields) {
|
|
|
11232
11464
|
}
|
|
11233
11465
|
|
|
11234
11466
|
// src/app/steps/persist.ts
|
|
11235
|
-
import { convertToLlm as
|
|
11236
|
-
|
|
11237
|
-
// src/ui/error-format.ts
|
|
11238
|
-
var MAX_ERROR_TEXT = 240;
|
|
11239
|
-
var DEBUG_HINT = "Conversation unchanged. Set DEBUG=smart-compact for stack diagnostics.";
|
|
11240
|
-
function compactText(error2) {
|
|
11241
|
-
const text = error2 instanceof Error ? error2.message : String(error2);
|
|
11242
|
-
return text.replace(/\s+/g, " ").trim().slice(0, MAX_ERROR_TEXT) || "Unknown error";
|
|
11243
|
-
}
|
|
11244
|
-
function formatCompactErrorForUi(error2) {
|
|
11245
|
-
if (error2 instanceof VerificationGateError) {
|
|
11246
|
-
const kinds = error2.gapKinds.slice(0, 4).join(", ") || "unknown";
|
|
11247
|
-
return "Verification stopped apply at the " + error2.stage + " gate: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. " + DEBUG_HINT;
|
|
11248
|
-
}
|
|
11249
|
-
if (error2 instanceof YieldGateError) {
|
|
11250
|
-
const reason = error2.reason === "target-miss" ? "target missed" : "saving below 10%";
|
|
11251
|
-
return "Yield check stopped apply: estimated " + error2.estimatedAfterTokens.toLocaleString() + "t after vs " + error2.targetAfterTokens.toLocaleString() + "t target (" + reason + "). " + DEBUG_HINT;
|
|
11252
|
-
}
|
|
11253
|
-
return "Smart compact failed: " + compactText(error2) + ". " + DEBUG_HINT;
|
|
11254
|
-
}
|
|
11255
|
-
|
|
11256
|
-
// src/app/steps/persist.ts
|
|
11467
|
+
import { convertToLlm as convertToLlm4 } from "@earendil-works/pi-coding-agent";
|
|
11257
11468
|
async function persistAppliedState(pending) {
|
|
11258
11469
|
if (!pending.projectId)
|
|
11259
11470
|
return pending.compactionState || pending.extraction ? ["project state (project identity unavailable)"] : [];
|
|
@@ -11292,7 +11503,7 @@ async function commitAppliedCompaction(pending) {
|
|
|
11292
11503
|
}
|
|
11293
11504
|
function runDamageDetection(rc) {
|
|
11294
11505
|
try {
|
|
11295
|
-
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) =>
|
|
11506
|
+
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm4([asBranchMessage(e.message)])).flat();
|
|
11296
11507
|
if (postCompactMsgs.length <= 2)
|
|
11297
11508
|
return;
|
|
11298
11509
|
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
@@ -11582,7 +11793,7 @@ async function runSmartCompact(opts) {
|
|
|
11582
11793
|
const dur = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
|
|
11583
11794
|
const hasPending = base.pendingRef.isPresent(runSessionId);
|
|
11584
11795
|
if (hasPending || runFailed || finalRc)
|
|
11585
|
-
base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : runFailed ? "Smart compact stopped safely in " + dur + " \xB7 no summary applied \xB7 Pi fallback continues" : "Smart compact run finished in " + dur, runFailed ? "warning" : "info");
|
|
11796
|
+
base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : runFailed ? "Smart compact stopped safely in " + dur + (base.flags.skipCompact ? " \xB7 no summary staged \xB7 context unchanged" : " \xB7 no summary applied \xB7 Pi fallback continues") : "Smart compact run finished in " + dur, runFailed ? "warning" : "info");
|
|
11586
11797
|
}
|
|
11587
11798
|
}
|
|
11588
11799
|
}
|
|
@@ -12867,6 +13078,9 @@ function buildMetricsReport(entries = readMetricsLog(100), damageEntries, prebui
|
|
|
12867
13078
|
"## Stage provider/model comparison",
|
|
12868
13079
|
...providerRoutes.length ? providerRoutes : ["- No stage-route evidence yet"],
|
|
12869
13080
|
"",
|
|
13081
|
+
"## Recent provider call failures (not compaction outcomes)",
|
|
13082
|
+
...entries.slice(-20).flatMap((entry) => (entry.providerRoutes ?? []).filter((route) => route.successes < route.calls).map((route) => "- " + route.stage + " / " + route.provider + "/" + route.model + ": " + (route.calls - route.successes) + "/" + route.calls + " calls failed; " + (route.failures ? JSON.stringify(route.failures) : "legacy cause unclassified"))),
|
|
13083
|
+
"",
|
|
12870
13084
|
"## Failure taxonomy",
|
|
12871
13085
|
...Object.keys(insights.failures).length ? Object.entries(insights.failures).map(([kind, count]) => "- " + kind + ": " + count) : ["- No schema-v2 failures classified"]
|
|
12872
13086
|
].join(`
|
|
@@ -13115,7 +13329,7 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
13115
13329
|
pi.registerTool({
|
|
13116
13330
|
name: "smart_compact",
|
|
13117
13331
|
label: "Smart Compact",
|
|
13118
|
-
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification.
|
|
13332
|
+
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Prepares and stages a verified summary for the next /compact; this tool does not apply compaction mid-turn. The staged summary expires after 5 minutes. Call only when actual context usage is high; tool=XX% is tool-output ratio, not context fullness. Checks the configured context threshold before starting.",
|
|
13119
13333
|
promptSnippet: "Smart compaction",
|
|
13120
13334
|
promptGuidelines: [
|
|
13121
13335
|
"Use only when actual context usage is high (for example pi-auto-context context>=60%).",
|
|
@@ -13196,7 +13410,7 @@ Dashboard: ` + dashboard : ""));
|
|
|
13196
13410
|
const resolvedMode = mode ?? config.mode;
|
|
13197
13411
|
const sessionId = resolveSessionId(ctx);
|
|
13198
13412
|
if (!dryRun && pendingRef.peek(sessionId)?.sessionId === sessionId) {
|
|
13199
|
-
return textResult("A smart summary is already staged
|
|
13413
|
+
return textResult("A smart summary is already staged; context is unchanged. Run /compact before the 5-minute staging TTL expires to apply it. No LLM calls were made.");
|
|
13200
13414
|
}
|
|
13201
13415
|
const usage = ctx.getContextUsage?.();
|
|
13202
13416
|
const totalTokens = usage?.tokens ?? 0;
|
|
@@ -13206,7 +13420,7 @@ Dashboard: ` + dashboard : ""));
|
|
|
13206
13420
|
return textResult("Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + percent2 + "%). No action needed.");
|
|
13207
13421
|
}
|
|
13208
13422
|
if (contextPercent < config.minContextPercent) {
|
|
13209
|
-
return textResult("
|
|
13423
|
+
return textResult("Compaction skipped: context " + percent2 + "% (" + totalTokens.toLocaleString() + " / " + (ctx.model?.contextWindow ?? 0).toLocaleString() + " tokens), below the " + config.minContextPercent + "% agent-tool threshold. tool=XX% measures tool-output ratio, not context usage. " + "For deliberate early compaction, the user can run /smart-compact; preview and safety checks still apply.");
|
|
13210
13424
|
}
|
|
13211
13425
|
const current = ctx.model;
|
|
13212
13426
|
const { segModel, sumModel, verifyModel } = resolveModels(ctx, current, config);
|
|
@@ -13239,7 +13453,7 @@ Dashboard: ` + dashboard : ""));
|
|
|
13239
13453
|
content: [
|
|
13240
13454
|
{
|
|
13241
13455
|
type: "text",
|
|
13242
|
-
text: "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014
|
|
13456
|
+
text: "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 staged, not applied, for " + Math.round(FIVE_MINUTES_MS / 60000) + " min. Context is unchanged. Run /compact within that time to apply it; expiry discards the candidate."
|
|
13243
13457
|
}
|
|
13244
13458
|
],
|
|
13245
13459
|
details: staged.details
|
|
@@ -14767,19 +14981,29 @@ function smartCompactExtension(pi) {
|
|
|
14767
14981
|
const settledAutoTrigger = createSettledAutoTrigger();
|
|
14768
14982
|
const policy = createSmartCompactPolicy(pi);
|
|
14769
14983
|
const nativeContinuity = createNativeContinuityBridge();
|
|
14984
|
+
const applyFailureWrites = new Map;
|
|
14770
14985
|
const recordApplyFailure = (pending, reason) => {
|
|
14771
14986
|
if (!pending.metricsSnapshot)
|
|
14772
|
-
return;
|
|
14987
|
+
return null;
|
|
14773
14988
|
const cancelled = reason === "aborted" || reason === "shutdown";
|
|
14774
|
-
appendMetricsSnapshot(pending.sessionId, {
|
|
14989
|
+
const write = appendMetricsSnapshot(pending.sessionId, {
|
|
14775
14990
|
...pending.metricsSnapshot,
|
|
14776
14991
|
status: cancelled ? "cancelled" : "error",
|
|
14777
14992
|
failureKind: cancelled ? "cancelled" : reason === "evicted" ? "internal" : "persistence",
|
|
14778
14993
|
fallbackReason: "native-apply:" + reason
|
|
14779
14994
|
});
|
|
14995
|
+
applyFailureWrites.set(pending.runId, write);
|
|
14996
|
+
write.finally(() => {
|
|
14997
|
+
if (applyFailureWrites.get(pending.runId) === write) {
|
|
14998
|
+
applyFailureWrites.delete(pending.runId);
|
|
14999
|
+
}
|
|
15000
|
+
});
|
|
15001
|
+
return write;
|
|
14780
15002
|
};
|
|
14781
15003
|
const commitCandidates = createCompactionCommitStore({
|
|
14782
|
-
onDiscard:
|
|
15004
|
+
onDiscard: (pending, reason) => {
|
|
15005
|
+
recordApplyFailure(pending, reason);
|
|
15006
|
+
}
|
|
14783
15007
|
});
|
|
14784
15008
|
const onNativeApplyError = (runId) => Boolean(commitCandidates.discard(runId, "apply-error"));
|
|
14785
15009
|
const activateOnlineDamage = (pending) => {
|
|
@@ -14887,7 +15111,6 @@ function smartCompactExtension(pi) {
|
|
|
14887
15111
|
onNativeApplyError,
|
|
14888
15112
|
autoTriggered: true,
|
|
14889
15113
|
overflowRecovery: event.reason === "overflow",
|
|
14890
|
-
maxLlmCalls: Math.min(config.maxLlmCalls, AUTO_TRIGGER_MAX_LLM_CALLS),
|
|
14891
15114
|
timeoutMs: effectiveTimeoutMs,
|
|
14892
15115
|
abortSignal: event.signal,
|
|
14893
15116
|
cancellationOut
|
|
@@ -14954,6 +15177,24 @@ function smartCompactExtension(pi) {
|
|
|
14954
15177
|
if (state)
|
|
14955
15178
|
nativeContinuity.stage({ projectId, sessionId, branchHeadId }, renderContinuityCapsule(state));
|
|
14956
15179
|
});
|
|
15180
|
+
const compactFailedEvents = pi;
|
|
15181
|
+
compactFailedEvents.on("session_compact_failed", async (event, ctx) => {
|
|
15182
|
+
if (!event.fromExtension)
|
|
15183
|
+
return;
|
|
15184
|
+
const sessionId = resolveSessionId(ctx);
|
|
15185
|
+
clearCompactProgress(ctx);
|
|
15186
|
+
const reason = event.aborted ? "aborted" : "apply-error";
|
|
15187
|
+
const discarded = commitCandidates.clearSession(sessionId, reason);
|
|
15188
|
+
const metricWrites = discarded.flatMap((pending) => {
|
|
15189
|
+
const write = applyFailureWrites.get(pending.runId);
|
|
15190
|
+
return write ? [write] : [];
|
|
15191
|
+
});
|
|
15192
|
+
if (metricWrites.length > 0)
|
|
15193
|
+
await Promise.all(metricWrites);
|
|
15194
|
+
if (discarded.length > 0) {
|
|
15195
|
+
warn("Discarded " + discarded.length + " staged smart compaction after native apply " + (event.aborted ? "abort" : "failure") + (event.errorMessage ? ": " + event.errorMessage : ""));
|
|
15196
|
+
}
|
|
15197
|
+
});
|
|
14957
15198
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
14958
15199
|
const scope = resolveGraphScope(ctx);
|
|
14959
15200
|
if (!scope?.branchHeadId)
|
|
@@ -14982,7 +15223,7 @@ function smartCompactExtension(pi) {
|
|
|
14982
15223
|
pi.on("message_end", async (event, ctx) => {
|
|
14983
15224
|
try {
|
|
14984
15225
|
const sessionId = resolveSessionId(ctx);
|
|
14985
|
-
const converted =
|
|
15226
|
+
const converted = convertToLlm5([event.message])[0];
|
|
14986
15227
|
if (!converted)
|
|
14987
15228
|
return;
|
|
14988
15229
|
const observation = damageMonitor.observe(sessionId, converted);
|