pi-smart-compact 9.2.1 → 9.3.1

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/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
7
7
  import { Type as Type2 } from "typebox";
8
8
 
9
9
  // src/constants.ts
10
- var VERSION = "9.2.1";
10
+ var VERSION = "9.3.1";
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;
@@ -585,8 +585,9 @@ import fs2 from "fs";
585
585
 
586
586
  // src/infra/paths.ts
587
587
  import path2 from "path";
588
+ import os from "os";
588
589
  function home() {
589
- return process.env.HOME ?? "/tmp";
590
+ return process.env.HOME ?? os.homedir();
590
591
  }
591
592
  function piAgentDir() {
592
593
  return path2.join(home(), ".pi", "agent");
@@ -1131,10 +1132,22 @@ function buildUniquePathNeedles(filePath, allPaths) {
1131
1132
  });
1132
1133
  }
1133
1134
  function isKnownPathReference(ref, knownPaths) {
1134
- const normalizedRef = normalizePath(ref);
1135
+ const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
1136
+ if (!normalizedRef)
1137
+ return false;
1138
+ const pathShaped = normalizedRef.includes("/");
1135
1139
  return knownPaths.some((path3) => {
1136
- const normalizedPath = normalizePath(path3);
1137
- return normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef);
1140
+ const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
1141
+ if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
1142
+ return true;
1143
+ if (normalizedPath.endsWith(normalizedRef)) {
1144
+ const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
1145
+ if (boundary && !/[\w./-]/.test(boundary))
1146
+ return true;
1147
+ }
1148
+ if (!pathShaped)
1149
+ return false;
1150
+ return normalizedPath.split("/").some((_, index, parts) => parts.slice(index).join("/").startsWith(normalizedRef + "/"));
1138
1151
  });
1139
1152
  }
1140
1153
 
@@ -1407,10 +1420,20 @@ function isLikelyFileRef(candidate) {
1407
1420
  return CODE_EXT_RE.test(candidate);
1408
1421
  }
1409
1422
  function extractFileRefs(summary) {
1410
- const candidates = summary.match(FILE_REF_CANDIDATE_RE) ?? [];
1411
- return candidates.filter(isLikelyFileRef);
1423
+ const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
1424
+ const refs = [];
1425
+ for (const match of summary.matchAll(matcher)) {
1426
+ if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
1427
+ continue;
1428
+ if (isLikelyFileRef(match[0]))
1429
+ refs.push(match[0]);
1430
+ }
1431
+ return refs;
1412
1432
  }
1413
1433
 
1434
+ // src/domain/summary-parse.ts
1435
+ import { createHash } from "crypto";
1436
+
1414
1437
  // src/domain/summary-schema.ts
1415
1438
  function classifyHeading(raw) {
1416
1439
  const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
@@ -1483,6 +1506,58 @@ var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
1483
1506
  function summaryEvidenceLine(value, maxLength) {
1484
1507
  return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
1485
1508
  }
1509
+ function summaryPathLine(value) {
1510
+ return JSON.stringify(value);
1511
+ }
1512
+ function compactPathLine(value, maxLength, digest) {
1513
+ const minimal = JSON.stringify("#" + digest);
1514
+ if (minimal.length >= maxLength)
1515
+ return minimal;
1516
+ const chars = Array.from(value.replace(/\\/g, "/"));
1517
+ let low = 0;
1518
+ let high = chars.length;
1519
+ let best = minimal;
1520
+ while (low <= high) {
1521
+ const length = Math.floor((low + high) / 2);
1522
+ const candidate = JSON.stringify("\u2026/" + chars.slice(-length).join("") + "#" + digest);
1523
+ if (candidate.length <= maxLength) {
1524
+ best = candidate;
1525
+ low = length + 1;
1526
+ } else {
1527
+ high = length - 1;
1528
+ }
1529
+ }
1530
+ return best;
1531
+ }
1532
+ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summaryBudgetTokens) {
1533
+ const unique = Array.from(new Set(paths.filter(Boolean)));
1534
+ if (!unique.length)
1535
+ return new Map;
1536
+ const full = unique.map((path3) => [path3, summaryPathLine(path3)]);
1537
+ const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
1538
+ const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
1539
+ if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
1540
+ return new Map(full);
1541
+ }
1542
+ const digests = new Map;
1543
+ const owners = new Map;
1544
+ for (const path3 of unique) {
1545
+ const fullDigest = createHash("sha256").update(path3).digest("base64url");
1546
+ let digest = fullDigest.slice(0, 12);
1547
+ const owner = owners.get(digest);
1548
+ if (owner && owner !== path3) {
1549
+ digest = fullDigest;
1550
+ digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
1551
+ }
1552
+ owners.set(digest, path3);
1553
+ digests.set(path3, digest);
1554
+ }
1555
+ const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
1556
+ return new Map(unique.map((path3) => [
1557
+ path3,
1558
+ compactPathLine(path3, perPath, digests.get(path3) ?? "")
1559
+ ]));
1560
+ }
1486
1561
  function mergeBodies(first, second) {
1487
1562
  const seen = new Set;
1488
1563
  return [first, second].filter(Boolean).flatMap((body) => body.split(`
@@ -1507,7 +1582,11 @@ function parseSummary(markdown) {
1507
1582
  if (existing)
1508
1583
  existing.body = mergeBodies(existing.body, body);
1509
1584
  else
1510
- sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
1585
+ sections.push({
1586
+ kind: currentKind,
1587
+ heading: currentHeading.trim(),
1588
+ body
1589
+ });
1511
1590
  };
1512
1591
  for (const line of lines) {
1513
1592
  const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
@@ -1674,7 +1753,14 @@ function extractMediaAttachments(msgs) {
1674
1753
  const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
1675
1754
  const kind = mediaKind(type, mimeType);
1676
1755
  if (kind !== "unknown" || source || mimeType || name) {
1677
- out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
1756
+ out.push({
1757
+ index: i,
1758
+ kind,
1759
+ mimeType,
1760
+ name,
1761
+ sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined,
1762
+ source
1763
+ });
1678
1764
  }
1679
1765
  }
1680
1766
  }
@@ -1698,7 +1784,11 @@ function buildToolCallIndex(msgs) {
1698
1784
  for (let t = 0;t < nested.length; t++) {
1699
1785
  const tool = nested[t];
1700
1786
  const id = nestedToolCallId(b.id, i, t, tool.id);
1701
- idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
1787
+ idx.set(id, {
1788
+ name: tool.name,
1789
+ arguments: tool.arguments,
1790
+ msgIndex: i
1791
+ });
1702
1792
  }
1703
1793
  }
1704
1794
  }
@@ -1729,7 +1819,10 @@ function trackFileOps(msgs, _tcIdx) {
1729
1819
  const shell = extractShellFileOperations(tc.arguments);
1730
1820
  for (const file of shell.modified) {
1731
1821
  const existing = modMap.get(file);
1732
- modMap.set(file, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
1822
+ modMap.set(file, {
1823
+ toolCalls: (existing?.toolCalls ?? 0) + 1,
1824
+ lastIdx: i
1825
+ });
1733
1826
  deletedAt.delete(file);
1734
1827
  }
1735
1828
  for (const file of shell.deleted) {
@@ -1746,7 +1839,10 @@ function trackFileOps(msgs, _tcIdx) {
1746
1839
  const resultText = extractText(m.content);
1747
1840
  if (isTruncated(resultText) || !NO_OP_RE.test(resultText)) {
1748
1841
  const existing = modMap.get(filePath);
1749
- modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
1842
+ modMap.set(filePath, {
1843
+ toolCalls: (existing?.toolCalls ?? 0) + 1,
1844
+ lastIdx: i
1845
+ });
1750
1846
  deletedAt.delete(filePath);
1751
1847
  }
1752
1848
  } else if (operation === "delete") {
@@ -1759,7 +1855,11 @@ function trackFileOps(msgs, _tcIdx) {
1759
1855
  }
1760
1856
  }
1761
1857
  return {
1762
- modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
1858
+ modified: [...modMap.entries()].map(([p, d]) => ({
1859
+ path: p,
1860
+ toolCalls: d.toolCalls,
1861
+ lastModifiedIndex: d.lastIdx
1862
+ })),
1763
1863
  read: [...readAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
1764
1864
  deleted: [...deletedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file),
1765
1865
  referenced: [...referencedAt.entries()].sort((a, b) => a[1] - b[1]).map(([file]) => file)
@@ -1851,10 +1951,10 @@ function catalogErrors(msgs, _tcIdx) {
1851
1951
  const result = msgs[k];
1852
1952
  if (result?.role !== "toolResult" || result.isError)
1853
1953
  continue;
1854
- const resolved = retryTool.id != null ? result.toolCallId === retryTool.id : (() => {
1954
+ const resolved = retryTool.id == null ? (() => {
1855
1955
  const resultCall = tcIdx.get(result.toolCallId ?? "");
1856
1956
  return Boolean(resultCall && sameToolOperation(retryTool, resultCall));
1857
- })();
1957
+ })() : result.toolCallId === retryTool.id;
1858
1958
  if (resolved) {
1859
1959
  err.resolved = true;
1860
1960
  break;
@@ -1877,7 +1977,12 @@ function extractDecisions(msgs, _tcIdx) {
1877
1977
  continue;
1878
1978
  for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
1879
1979
  if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
1880
- decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, TRUNC.DECISION_SUMMARY), userResponse: extractText(msgs[i].content).slice(0, TRUNC.USER_RESPONSE) });
1980
+ decisions.push({
1981
+ index: tc.msgIndex,
1982
+ type: "explicit",
1983
+ summary: question.slice(0, TRUNC.DECISION_SUMMARY),
1984
+ userResponse: extractText(msgs[i].content).slice(0, TRUNC.USER_RESPONSE)
1985
+ });
1881
1986
  break;
1882
1987
  }
1883
1988
  }
@@ -1887,18 +1992,46 @@ function extractDecisions(msgs, _tcIdx) {
1887
1992
  continue;
1888
1993
  const txt = extractText(msgs[i].content);
1889
1994
  if (CHOICE_RE.test(txt)) {
1890
- decisions.push({ index: i, type: "implicit", summary: txt.slice(0, TRUNC.DECISION_SUMMARY) });
1995
+ decisions.push({
1996
+ index: i,
1997
+ type: "implicit",
1998
+ summary: txt.slice(0, TRUNC.DECISION_SUMMARY)
1999
+ });
1891
2000
  }
1892
2001
  }
1893
2002
  return decisions;
1894
2003
  }
1895
2004
  var CONSTRAINT_PATTERNS = [
1896
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
1897
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
1898
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
1899
- { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
1900
- { re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
1901
- { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
2005
+ {
2006
+ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i,
2007
+ cat: "requirement",
2008
+ conf: TUNING.CONFIDENCE_HIGH
2009
+ },
2010
+ {
2011
+ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i,
2012
+ cat: "prohibition",
2013
+ conf: TUNING.CONFIDENCE_MEDIUM
2014
+ },
2015
+ {
2016
+ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i,
2017
+ cat: "preference",
2018
+ conf: TUNING.CONFIDENCE_LOW
2019
+ },
2020
+ {
2021
+ re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu,
2022
+ cat: "prohibition",
2023
+ conf: TUNING.CONFIDENCE_MEDIUM
2024
+ },
2025
+ {
2026
+ re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu,
2027
+ cat: "requirement",
2028
+ conf: TUNING.CONFIDENCE_MEDIUM
2029
+ },
2030
+ {
2031
+ re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu,
2032
+ cat: "preference",
2033
+ conf: TUNING.CONFIDENCE_LOW
2034
+ }
1902
2035
  ];
1903
2036
  function isDiagnosticConstraintText(text) {
1904
2037
  const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
@@ -1923,7 +2056,12 @@ function mineConstraints(msgs) {
1923
2056
  const normalized = candidate.toLowerCase().replace(/\s+/g, " ");
1924
2057
  if (!seen.has(normalized)) {
1925
2058
  seen.add(normalized);
1926
- constraints.push({ index: i, text: candidate.slice(0, TRUNC.CONSTRAINT_TEXT), category: cat, confidence: conf });
2059
+ constraints.push({
2060
+ index: i,
2061
+ text: candidate.slice(0, TRUNC.CONSTRAINT_TEXT),
2062
+ category: cat,
2063
+ confidence: conf
2064
+ });
1927
2065
  }
1928
2066
  break;
1929
2067
  }
@@ -1932,6 +2070,12 @@ function mineConstraints(msgs) {
1932
2070
  return constraints;
1933
2071
  }
1934
2072
  function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2073
+ const shiftBasename = (filePath) => {
2074
+ if (!filePath)
2075
+ return null;
2076
+ const base = path3.basename(filePath);
2077
+ return GENERIC_BASENAMES.has(base.toLowerCase()) ? null : base;
2078
+ };
1935
2079
  const topics = [];
1936
2080
  let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
1937
2081
  let currentType = "exploration";
@@ -1943,7 +2087,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
1943
2087
  const messageTokens = estimateTokens(text);
1944
2088
  const tools = message.role === "assistant" ? (Array.isArray(message.content) ? message.content : []).flatMap(flattenToolCallBlock) : [];
1945
2089
  const nextFile = tools.map((tool) => extractToolPath(tool.arguments)).find((value) => Boolean(value));
1946
- const nextBasename = nextFile ? path3.basename(nextFile) : null;
2090
+ const nextBasename = shiftBasename(nextFile);
1947
2091
  const closesActiveTool = message.role === "toolResult" && (tcIdx.get(message.toolCallId ?? "")?.msgIndex ?? -1) >= startIdx;
1948
2092
  const fileShift = Boolean(lastFile && nextBasename && nextBasename !== lastFile);
1949
2093
  const userShift = message.role === "user" && SHIFT_RE.test(text);
@@ -1968,7 +2112,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
1968
2112
  for (const tool of tools) {
1969
2113
  const filePath = extractToolPath(tool.arguments);
1970
2114
  if (filePath) {
1971
- lastFile = path3.basename(filePath);
2115
+ lastFile = shiftBasename(filePath) ?? lastFile;
1972
2116
  currentPrimaryFile = filePath;
1973
2117
  }
1974
2118
  const operation = classifyToolOperation(tool.arguments, tool.name);
@@ -1991,7 +2135,13 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
1991
2135
  }
1992
2136
  }
1993
2137
  if (startIdx < msgs.length) {
1994
- topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: currentPrimaryFile, type: currentType, errorDensity: errAcc });
2138
+ topics.push({
2139
+ startIndex: startIdx,
2140
+ endIndex: msgs.length - 1,
2141
+ primaryFile: currentPrimaryFile,
2142
+ type: currentType,
2143
+ errorDensity: errAcc
2144
+ });
1995
2145
  }
1996
2146
  return topics;
1997
2147
  }
@@ -2003,10 +2153,18 @@ function buildTimeline(msgs, errors) {
2003
2153
  if (m.role === "user") {
2004
2154
  const txt = extractText(m.content);
2005
2155
  if (!txt.startsWith("/"))
2006
- timeline.push({ index: i, event: "user_request", summary: txt.slice(0, TRUNC.TIMELINE_EVENT) });
2156
+ timeline.push({
2157
+ index: i,
2158
+ event: "user_request",
2159
+ summary: txt.slice(0, TRUNC.TIMELINE_EVENT)
2160
+ });
2007
2161
  }
2008
2162
  if (errorIndices.has(i))
2009
- timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, TRUNC.TIMELINE_ERROR) ?? "error" });
2163
+ timeline.push({
2164
+ index: i,
2165
+ event: "error",
2166
+ summary: errors.find((e) => e.index === i)?.message.slice(0, TRUNC.TIMELINE_ERROR) ?? "error"
2167
+ });
2010
2168
  }
2011
2169
  return timeline.length > 30 ? [
2012
2170
  ...timeline.filter((t) => t.event === "user_request").slice(-TRUNC.TIMELINE_DISPLAY),
@@ -2080,6 +2238,13 @@ function extractOpenLoops(msgs, extraction) {
2080
2238
  sourceIndex: err.index
2081
2239
  });
2082
2240
  }
2241
+ const isNearbyDuplicate = (existing, text, index) => {
2242
+ if (Math.abs((existing.sourceIndex ?? 0) - index) >= 5)
2243
+ return false;
2244
+ const existingKey = normalizeFactKey(existing.summary);
2245
+ const textKey = normalizeFactKey(text);
2246
+ return existingKey === textKey || existingKey.includes(textKey) || textKey.includes(existingKey);
2247
+ };
2083
2248
  const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|yapalim|yapal\u0131m|yapmamiz|yapmam\u0131z|gerekiyor|eklenecek|d\u00FCzeltilecek|duzeltilecek|bitmedi|kaldi|kald\u0131)/iu;
2084
2249
  for (let idx = 0;idx < msgs.length; idx++) {
2085
2250
  const msg = msgs[idx];
@@ -2089,7 +2254,7 @@ function extractOpenLoops(msgs, extraction) {
2089
2254
  if (txt.length < 10 || txt.startsWith("/"))
2090
2255
  continue;
2091
2256
  if (FOLLOWUP_RE.test(txt)) {
2092
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2257
+ const isDup = loops.some((l) => isNearbyDuplicate(l, txt, idx));
2093
2258
  if (!isDup) {
2094
2259
  loops.push({
2095
2260
  id: ID_PREFIX.OPEN_LOOP + ++loopId,
@@ -2112,7 +2277,7 @@ function extractOpenLoops(msgs, extraction) {
2112
2277
  if (txt.length < 10 || txt.startsWith("/"))
2113
2278
  continue;
2114
2279
  if (BLOCKED_RE.test(txt)) {
2115
- const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
2280
+ const isDup = loops.some((l) => isNearbyDuplicate(l, txt, idx));
2116
2281
  if (!isDup) {
2117
2282
  loops.push({
2118
2283
  id: ID_PREFIX.OPEN_LOOP + ++loopId,
@@ -2126,20 +2291,6 @@ function extractOpenLoops(msgs, extraction) {
2126
2291
  }
2127
2292
  }
2128
2293
  }
2129
- for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
2130
- const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
2131
- if (!exists) {
2132
- loops.push({
2133
- id: ID_PREFIX.OPEN_LOOP + ++loopId,
2134
- type: "retry",
2135
- priority: "high",
2136
- status: "open",
2137
- summary: "Retried but unresolved: " + err.message.slice(0, TRUNC.SNIPPET),
2138
- files: [],
2139
- sourceIndex: err.index
2140
- });
2141
- }
2142
- }
2143
2294
  for (const loop of loops) {
2144
2295
  if (loop.type !== "follow-up" || loop.sourceIndex == null)
2145
2296
  continue;
@@ -2149,8 +2300,12 @@ function extractOpenLoops(msgs, extraction) {
2149
2300
  const end = Math.min(msgs.length, loop.sourceIndex + 50);
2150
2301
  for (let index = loop.sourceIndex + 1;index < end; index++) {
2151
2302
  const message = msgs[index];
2152
- if (message?.role === "user")
2303
+ if (message?.role === "user") {
2304
+ const turn = extractText(message.content);
2305
+ if (turn.length < 10 || ACK_ONLY_RE.test(turn))
2306
+ continue;
2153
2307
  break;
2308
+ }
2154
2309
  if (message?.role !== "assistant")
2155
2310
  continue;
2156
2311
  const response = extractText(message.content);
@@ -3065,7 +3220,10 @@ function withCodexWireLimit(model, opts) {
3065
3220
  onPayload: async (payload, requestModel) => {
3066
3221
  const transformed = await previous?.(payload, requestModel);
3067
3222
  const body = transformed ?? payload;
3068
- return body && typeof body === "object" ? { ...body, max_output_tokens: opts.maxTokens } : body;
3223
+ return body && typeof body === "object" ? {
3224
+ ...body,
3225
+ max_output_tokens: opts.maxTokens
3226
+ } : body;
3069
3227
  }
3070
3228
  };
3071
3229
  }
@@ -3086,11 +3244,12 @@ function assertSuccessful(message) {
3086
3244
  }
3087
3245
  return message;
3088
3246
  }
3089
- async function withProviderDeadline(opts, invoke) {
3247
+ async function withProviderDeadline(opts, invoke, modelId) {
3090
3248
  if (opts.signal?.aborted)
3091
3249
  throw new Error("LLM request aborted before dispatch");
3092
3250
  const controller = new AbortController;
3093
- const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
3251
+ const multiplier = modelId && !((opts.codexWatchdogMs ?? 0) > 0) ? getProviderCaps(modelId).timeoutMultiplier : 1;
3252
+ const watchdogMs = Math.round(resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs) * multiplier);
3094
3253
  const abort = Promise.withResolvers();
3095
3254
  const abortFromCaller = () => {
3096
3255
  controller.abort(opts.signal?.reason);
@@ -3163,7 +3322,7 @@ var rawLlmClient = {
3163
3322
  return completeChatGptCodex(model, body, bounded);
3164
3323
  const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
3165
3324
  return assertSuccessful(response);
3166
- });
3325
+ }, model.id);
3167
3326
  }
3168
3327
  };
3169
3328
  var defaultLlmClient = rawLlmClient;
@@ -3177,7 +3336,10 @@ import crypto4 from "crypto";
3177
3336
 
3178
3337
  // src/domain/scrub.ts
3179
3338
  var SECRET_PATTERNS = [
3180
- { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
3339
+ {
3340
+ kind: "private-key",
3341
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
3342
+ },
3181
3343
  { kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
3182
3344
  { kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
3183
3345
  { kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
@@ -3186,8 +3348,15 @@ var SECRET_PATTERNS = [
3186
3348
  { kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
3187
3349
  { kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
3188
3350
  { kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
3189
- { kind: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
3190
- { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}=*/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
3351
+ {
3352
+ kind: "jwt",
3353
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
3354
+ },
3355
+ {
3356
+ kind: "bearer-token",
3357
+ regex: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
3358
+ replacement: () => "Bearer [REDACTED:bearer-token]"
3359
+ },
3191
3360
  {
3192
3361
  kind: "connection-password",
3193
3362
  regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
@@ -3196,12 +3365,35 @@ var SECRET_PATTERNS = [
3196
3365
  {
3197
3366
  kind: "credential",
3198
3367
  regex: /\b((?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|passwd|secret(?:[_-]?(?:access)?[_-]?key)?|client[_-]?secret)(?:[_-][A-Za-z0-9]+)*)\s*([:=])\s*["']?([^\s"']{16,})["']?/gi,
3199
- replacement: (name, separator) => name + separator + "[REDACTED:credential]"
3368
+ replacement: (name, separator, value, match) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) ? match : name + separator + "[REDACTED:credential]"
3200
3369
  }
3201
3370
  ];
3371
+ function passesLuhn(candidate) {
3372
+ const digits = candidate.replace(/\D/g, "");
3373
+ if (digits.length < 13 || digits.length > 19)
3374
+ return false;
3375
+ if (/^(\d)\1+$/.test(digits))
3376
+ return false;
3377
+ let sum = 0, double = false;
3378
+ for (let i = digits.length - 1;i >= 0; i--) {
3379
+ let d = digits.charCodeAt(i) - 48;
3380
+ if (double) {
3381
+ d *= 2;
3382
+ if (d > 9)
3383
+ d -= 9;
3384
+ }
3385
+ sum += d;
3386
+ double = !double;
3387
+ }
3388
+ return sum % 10 === 0;
3389
+ }
3202
3390
  var PII_PATTERNS = [
3203
3391
  { kind: "email", regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
3204
- { kind: "payment-card", regex: /\b(?:\d[ -]*?){13,19}\b/g },
3392
+ {
3393
+ kind: "payment-card",
3394
+ regex: /\b(?:\d[ -]*?){13,19}\b/g,
3395
+ replacement: (candidate) => passesLuhn(candidate) ? "[REDACTED:payment-card]" : candidate
3396
+ },
3205
3397
  { kind: "phone", regex: /(?<![\w.])(?:\+?\d[\d ()-]{8,}\d)(?![\w.])/g }
3206
3398
  ];
3207
3399
  function redact(text, patterns) {
@@ -3209,15 +3401,22 @@ function redact(text, patterns) {
3209
3401
  let value = text;
3210
3402
  for (const pattern of patterns) {
3211
3403
  value = value.replace(pattern.regex, (...args) => {
3212
- counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
3404
+ const match = String(args[0]);
3405
+ let replacement = "[REDACTED:" + pattern.kind + "]";
3213
3406
  if (pattern.replacement) {
3214
3407
  const groups = args.slice(1, -2).map(String);
3215
- return pattern.replacement(...groups);
3408
+ replacement = pattern.replacement(...groups, match);
3216
3409
  }
3217
- return "[REDACTED:" + pattern.kind + "]";
3410
+ if (replacement === match)
3411
+ return match;
3412
+ counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
3413
+ return replacement;
3218
3414
  });
3219
3415
  }
3220
- return { value, findings: [...counts].map(([kind, count]) => ({ kind, count })) };
3416
+ return {
3417
+ value,
3418
+ findings: [...counts].map(([kind, count]) => ({ kind, count }))
3419
+ };
3221
3420
  }
3222
3421
  function mergeFindings(target, findings) {
3223
3422
  for (const finding of findings)
@@ -3247,7 +3446,6 @@ var SECRET_KEY_NAMES = {
3247
3446
  set_cookie: true,
3248
3447
  otp: true,
3249
3448
  one_time_password: true,
3250
- pin: true,
3251
3449
  passcode: true
3252
3450
  };
3253
3451
  function normalizeObjectKey(key) {
@@ -3313,7 +3511,7 @@ class SecretScrubber {
3313
3511
  const output = {};
3314
3512
  seen.set(value2, output);
3315
3513
  for (const [key, item] of Object.entries(value2)) {
3316
- const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
3514
+ const carriesSecret = typeof item === "string" && item.length >= 8;
3317
3515
  if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
3318
3516
  output[key] = "[REDACTED:credential]";
3319
3517
  recordCredential();
@@ -3324,7 +3522,10 @@ class SecretScrubber {
3324
3522
  return output;
3325
3523
  };
3326
3524
  const value = visit(input);
3327
- return { value, findings: [...findings].map(([kind, count]) => ({ kind, count })) };
3525
+ return {
3526
+ value,
3527
+ findings: [...findings].map(([kind, count]) => ({ kind, count }))
3528
+ };
3328
3529
  }
3329
3530
  count() {
3330
3531
  return this.total;
@@ -4772,7 +4973,7 @@ function writeMetricsDashboard(entries = readMetricsLog(200), damageEntries = re
4772
4973
  import { randomUUID as randomUUID3 } from "crypto";
4773
4974
 
4774
4975
  // src/app/session-run-lock.ts
4775
- import { createHash, randomUUID } from "crypto";
4976
+ import { createHash as createHash2, randomUUID } from "crypto";
4776
4977
  import fs5 from "fs";
4777
4978
  import path6 from "path";
4778
4979
  function processAlive(pid) {
@@ -4898,7 +5099,7 @@ function createSessionRunLock(maxConcurrent = 2, options = {}) {
4898
5099
  return true;
4899
5100
  }
4900
5101
  try {
4901
- const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
5102
+ const sessionHash = createHash2("sha256").update(sessionId).digest("hex").slice(0, 24);
4902
5103
  const session = acquireFileLease(path6.join(leaseDir, "session-" + sessionHash + ".lock"), staleMs);
4903
5104
  if (!session)
4904
5105
  return false;
@@ -5937,7 +6138,15 @@ function sanitizeCompactionStateEvidence(state) {
5937
6138
  const criticalContext = state.criticalContext.filter((item) => !isNoise(item));
5938
6139
  if (goal === state.goal && constraints.length === state.constraints.length && unresolvedErrors.length === state.unresolvedErrors.length && resolvedErrors.length === state.resolvedErrors.length && openLoops.length === state.openLoops.length && criticalContext.length === state.criticalContext.length)
5939
6140
  return state;
5940
- return { ...state, goal, constraints, unresolvedErrors, resolvedErrors, openLoops, criticalContext };
6141
+ return {
6142
+ ...state,
6143
+ goal,
6144
+ constraints,
6145
+ unresolvedErrors,
6146
+ resolvedErrors,
6147
+ openLoops,
6148
+ criticalContext
6149
+ };
5941
6150
  }
5942
6151
  function freshState(fp, data) {
5943
6152
  if (!data)
@@ -6040,7 +6249,12 @@ function applyLoopOverrides(loops, overrides) {
6040
6249
  function upsertLoopOverride(overrides, loop, patch) {
6041
6250
  const summaryKey = normalizeFactKey(loop.summary);
6042
6251
  const index = overrides.findIndex((override) => override.summaryKey === summaryKey);
6043
- const next = { ...index >= 0 ? overrides[index] : { id: loop.id, summaryKey }, ...patch, id: loop.id, summaryKey };
6252
+ const next = {
6253
+ ...index >= 0 ? overrides[index] : { id: loop.id, summaryKey },
6254
+ ...patch,
6255
+ id: loop.id,
6256
+ summaryKey
6257
+ };
6044
6258
  if (index < 0)
6045
6259
  return [...overrides, next];
6046
6260
  const copy = overrides.slice();
@@ -6065,7 +6279,10 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
6065
6279
  let decisionId = 0;
6066
6280
  let constraintId = 0;
6067
6281
  let errorId = 0;
6068
- const fileNeedles = extraction.modifiedFiles.map((f) => ({ path: f.path, needles: buildPathNeedles(f.path) }));
6282
+ const fileNeedles = extraction.modifiedFiles.map((f) => ({
6283
+ path: f.path,
6284
+ needles: buildPathNeedles(f.path)
6285
+ }));
6069
6286
  return {
6070
6287
  goal: extraction.mainGoal,
6071
6288
  goalKey: extraction.mainGoal ? normalizeFactKey(extraction.mainGoal) : undefined,
@@ -6123,9 +6340,17 @@ function mergeBy(current, previous, key, limit) {
6123
6340
  return true;
6124
6341
  }).slice(0, limit);
6125
6342
  }
6126
- var LOOP_PRIORITY = { critical: 0, high: 1, normal: 2, low: 3 };
6343
+ var LOOP_PRIORITY = {
6344
+ critical: 0,
6345
+ high: 1,
6346
+ normal: 2,
6347
+ low: 3
6348
+ };
6127
6349
  function mergeOpenLoops(current, previous) {
6128
- return mergeBy(current, previous, (item) => normalizeFactKey(item.summary), Number.MAX_SAFE_INTEGER).map((item, order) => ({ item, order })).sort((a, b) => Number(a.item.status === "resolved") - Number(b.item.status === "resolved") || LOOP_PRIORITY[a.item.priority] - LOOP_PRIORITY[b.item.priority] || a.order - b.order).slice(0, MAX_STATE_OPEN_LOOPS).map(({ item }, index) => ({ ...item, id: ID_PREFIX.OPEN_LOOP + (index + 1) }));
6350
+ return mergeBy(current, previous, (item) => normalizeFactKey(item.summary), Number.MAX_SAFE_INTEGER).map((item, order) => ({ item, order })).sort((a, b) => Number(a.item.status === "resolved") - Number(b.item.status === "resolved") || LOOP_PRIORITY[a.item.priority] - LOOP_PRIORITY[b.item.priority] || a.order - b.order).slice(0, MAX_STATE_OPEN_LOOPS).map(({ item }, index) => ({
6351
+ ...item,
6352
+ id: ID_PREFIX.OPEN_LOOP + (index + 1)
6353
+ }));
6129
6354
  }
6130
6355
  function mergeCompactionStates(previous, current) {
6131
6356
  if (!previous) {
@@ -6138,13 +6363,27 @@ function mergeCompactionStates(previous, current) {
6138
6363
  const currentPresent = new Set([...activeCurrent.modifiedFiles, ...activeCurrent.readFiles].map(normalizeFactKey));
6139
6364
  const currentDeleted = new Set(activeCurrent.deletedFiles.map(normalizeFactKey));
6140
6365
  const resolvedKeys = new Set(activeCurrent.resolvedErrors.map((error2) => normalizeFactKey(error2.message)));
6366
+ const isResolvedLoop = (loop) => {
6367
+ if (loop.type !== "bugfix" || loop.status !== "open")
6368
+ return false;
6369
+ const summaryKey = normalizeFactKey(loop.summary);
6370
+ if (summaryKey.length < 16)
6371
+ return false;
6372
+ for (const key of resolvedKeys) {
6373
+ if (key === summaryKey || key.length > summaryKey.length && key.startsWith(summaryKey))
6374
+ return true;
6375
+ }
6376
+ return false;
6377
+ };
6141
6378
  const decisions = mergeBy(activeCurrent.decisions, activePrevious.decisions, (item) => normalizeFactKey(item.summary), 30).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) }));
6142
6379
  const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
6143
6380
  const unresolvedErrors = mergeBy(activeCurrent.unresolvedErrors, activePrevious.unresolvedErrors.filter((error2) => !resolvedKeys.has(normalizeFactKey(error2.message))), (item) => normalizeFactKey(item.message), 15).map((item, index) => ({ ...item, id: ID_PREFIX.ERROR + (index + 1) }));
6144
- const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops);
6381
+ const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops.map((loop) => isResolvedLoop(loop) ? { ...loop, status: "resolved" } : loop));
6145
6382
  const currentGoalKey = activeCurrent.goalKey ?? (activeCurrent.goal ? normalizeFactKey(activeCurrent.goal) : "");
6146
6383
  const previousGoalKey = activePrevious.goalKey ?? (activePrevious.goal ? normalizeFactKey(activePrevious.goal) : "");
6147
- const oldGoal = previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey ? ["Previous goal: " + activePrevious.goal] : [];
6384
+ const PREV_GOAL_PREFIX = "Previous goal: ";
6385
+ const durablePreviousContext = activePrevious.criticalContext.filter((line) => !line.startsWith(PREV_GOAL_PREFIX));
6386
+ const oldGoal = previousGoalKey && currentGoalKey && previousGoalKey !== currentGoalKey ? [PREV_GOAL_PREFIX + activePrevious.goal] : [];
6148
6387
  return applyContinuityOverrides({
6149
6388
  ...activeCurrent,
6150
6389
  goal: activeCurrent.goal ?? activePrevious.goal,
@@ -6161,7 +6400,7 @@ function mergeCompactionStates(previous, current) {
6161
6400
  factOverrides,
6162
6401
  topics: mergeBy(activeCurrent.topics, activePrevious.topics, (item) => normalizeFactKey(item.title), 30),
6163
6402
  nextActions: mergeBy(activeCurrent.nextActions, activePrevious.nextActions, normalizeFactKey, 15),
6164
- criticalContext: mergeBy([...oldGoal, ...activeCurrent.criticalContext], activePrevious.criticalContext, normalizeFactKey, 20),
6403
+ criticalContext: mergeBy([...oldGoal, ...activeCurrent.criticalContext], durablePreviousContext, normalizeFactKey, 20),
6165
6404
  updatedAt: Date.now()
6166
6405
  }, factOverrides);
6167
6406
  }
@@ -7210,7 +7449,7 @@ function findLogInDirectory(directory, sessionId) {
7210
7449
  return match ? path11.join(directory, match.name) : null;
7211
7450
  }
7212
7451
  function findSessionLogFile(sessionId, cwd) {
7213
- const home2 = process.env.HOME ?? "/tmp";
7452
+ const home2 = home();
7214
7453
  const now = Date.now();
7215
7454
  const directDirectory = cwd ? sessionDirectoryForCwd(cwd) : null;
7216
7455
  const cacheKey = sessionId + "\x00" + (directDirectory ?? "*");
@@ -7321,7 +7560,9 @@ async function resolveCompactionMessages(sessionId, toCompactEntries, cwd) {
7321
7560
  for (const entry of toCompactEntries) {
7322
7561
  if (!entry.id)
7323
7562
  continue;
7324
- const converted = convertToLlm([asBranchMessage(entry.message)]);
7563
+ const converted = convertToLlm([
7564
+ asBranchMessage(entry.message)
7565
+ ]);
7325
7566
  if (!converted.length)
7326
7567
  continue;
7327
7568
  const logMsg = logMap.get(entry.id);
@@ -7555,7 +7796,11 @@ function extractWithCache(rc) {
7555
7796
  return convText;
7556
7797
  const safeMessages = scrubLlmMessages(selectedMessages, rc.services.scrubber);
7557
7798
  const backupText = serializeConversation(asSerializableMessages(safeMessages));
7558
- return rc.services.scrubber.scrubText(backupText).value;
7799
+ const scrubbed = rc.services.scrubber.scrubText(backupText);
7800
+ if (scrubbed.findings.length > 0) {
7801
+ rc.notify("Backup written with redactions (" + scrubbed.findings.map((f) => f.count + "x " + f.kind).join(", ") + ") \u2014 restore will lack that data", "info");
7802
+ }
7803
+ return scrubbed.value;
7559
7804
  };
7560
7805
  preparedBackup = prepareConversationBackup(materializeBackup, rc.sessionId, {
7561
7806
  branchLeafId: branchEntryIds(rc.branch).at(-1),
@@ -7583,7 +7828,7 @@ function extractWithCache(rc) {
7583
7828
  cacheUsable = branchPrefixMatch && prunedPrefixMatch && boundedCacheShape && cachedExt.messageCount === keptCount && cachedExt.messageCount <= rc.llmMessages.length;
7584
7829
  cacheExact = cacheUsable && cachedExt.messageCount === rc.llmMessages.length;
7585
7830
  if (!cacheUsable) {
7586
- missReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : !boundedCacheShape ? "cache-evidence-unbounded" : cachedExt.messageCount !== keptCount ? "cache-shape-mismatch" : "cache-domain-ahead";
7831
+ missReason = branchPrefixMatch ? prunedPrefixMatch ? boundedCacheShape ? cachedExt.messageCount === keptCount ? "cache-domain-ahead" : "cache-shape-mismatch" : "cache-evidence-unbounded" : "pruned-prefix-changed" : "entry-prefix-mismatch";
7587
7832
  }
7588
7833
  } else {
7589
7834
  missReason = "legacy-no-kept-entryids";
@@ -8149,13 +8394,13 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
8149
8394
  }
8150
8395
 
8151
8396
  // src/infra/synthesis-cache.ts
8152
- import { createHash as createHash2 } from "crypto";
8397
+ import { createHash as createHash3 } from "crypto";
8153
8398
  var cache = new Map;
8154
8399
  var batchCache = new Map;
8155
8400
  var TTL_MS = 10 * 60000;
8156
8401
  var MAX_ENTRIES = 16;
8157
8402
  function fingerprint(value) {
8158
- return createHash2("sha256").update(value ?? "").digest("hex");
8403
+ return createHash3("sha256").update(value ?? "").digest("hex");
8159
8404
  }
8160
8405
  function cloneExplorationReport(report) {
8161
8406
  if (!report)
@@ -8219,7 +8464,7 @@ function synthesisCacheKey(rc) {
8219
8464
  userNote: rc.userNote,
8220
8465
  zeroCall: rc.config.zeroCallEnabled !== false
8221
8466
  });
8222
- return createHash2("sha256").update(payload).digest("hex");
8467
+ return createHash3("sha256").update(payload).digest("hex");
8223
8468
  }
8224
8469
  function getCachedSynthesis(key, now = Date.now()) {
8225
8470
  const entry = cache.get(key);
@@ -8244,7 +8489,7 @@ function setCachedSynthesis(key, value, now = Date.now()) {
8244
8489
  cache.set(key, { value: cloneSynthesis(value), createdAt: now });
8245
8490
  }
8246
8491
  function batchCacheKey(value) {
8247
- return createHash2("sha256").update(VERSION + `
8492
+ return createHash3("sha256").update(VERSION + `
8248
8493
  ` + JSON.stringify(value)).digest("hex");
8249
8494
  }
8250
8495
  function getCachedBatch(key, now = Date.now()) {
@@ -8309,9 +8554,12 @@ function fitChunkBudget(messages, maxTokens, estimator) {
8309
8554
  const head = Math.max(8, Math.floor(target * 0.6));
8310
8555
  const tail = Math.max(4, target - head);
8311
8556
  changed = true;
8312
- return { ...message, content: text.slice(0, head) + `
8557
+ return {
8558
+ ...message,
8559
+ content: text.slice(0, head) + `
8313
8560
  [\u2026tool evidence bounded for synthesis\u2026]
8314
- ` + text.slice(-tail) };
8561
+ ` + text.slice(-tail)
8562
+ };
8315
8563
  });
8316
8564
  if (!changed)
8317
8565
  break;
@@ -8326,12 +8574,14 @@ function fitChunkBudget(messages, maxTokens, estimator) {
8326
8574
  if (chars <= 0)
8327
8575
  return [{ role: "user", content: marker }];
8328
8576
  const head = Math.ceil(chars * 0.6);
8329
- return [{
8330
- role: "user",
8331
- content: rendered.slice(0, head) + `
8577
+ return [
8578
+ {
8579
+ role: "user",
8580
+ content: rendered.slice(0, head) + `
8332
8581
  ` + marker + `
8333
8582
  ` + rendered.slice(-(chars - head))
8334
- }];
8583
+ }
8584
+ ];
8335
8585
  };
8336
8586
  let best = candidate(0);
8337
8587
  if (estimateChunkTokens(best, estimator) > maxTokens) {
@@ -8410,7 +8660,10 @@ function splitOversizedChunk(ch, maxTokens, estimator) {
8410
8660
  });
8411
8661
  start = end;
8412
8662
  }
8413
- return parts.map((part, index) => ({ ...part, topic: ch.topic + " (part " + (index + 1) + "/" + parts.length + ")" }));
8663
+ return parts.map((part, index) => ({
8664
+ ...part,
8665
+ topic: ch.topic + " (part " + (index + 1) + "/" + parts.length + ")"
8666
+ }));
8414
8667
  }
8415
8668
  function chunkLlmMessages(msgs, boundaries, pc, estimator = makeTokenEstimator(), focus) {
8416
8669
  if (!msgs.length)
@@ -8506,10 +8759,23 @@ Preserve extra detail about: ` + focus + `
8506
8759
  const resp = await trackedComplete("single-pass", model, {
8507
8760
  systemPrompt: COMPACT_SYSTEM_PREFIX,
8508
8761
  messages: [
8509
- { role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
8510
- { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
8762
+ {
8763
+ role: "user",
8764
+ content: [{ type: "text", text: adaptedPrefix }],
8765
+ timestamp: Date.now()
8766
+ },
8767
+ {
8768
+ role: "user",
8769
+ content: [{ type: "text", text: dynamicSuffix }],
8770
+ timestamp: Date.now()
8771
+ }
8511
8772
  ]
8512
- }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budgetTokens, getProviderCaps(model.provider).maxOutputTokens), signal }, services);
8773
+ }, {
8774
+ apiKey: auth.apiKey,
8775
+ headers: auth.headers,
8776
+ maxTokens: Math.min(budgetTokens, getProviderCaps(model.provider).maxOutputTokens),
8777
+ signal
8778
+ }, services);
8513
8779
  const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
8514
8780
  `).trim();
8515
8781
  if (!summary.startsWith("##"))
@@ -8517,7 +8783,10 @@ Preserve extra detail about: ` + focus + `
8517
8783
  return { summary, llmCalls: 1 };
8518
8784
  }
8519
8785
  async function summarizeBatch(batch, extraction, model, auth, signal, services, maxOutputTokens, cacheScope) {
8520
- const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
8786
+ const range = {
8787
+ start: batch[0].startIndex,
8788
+ end: batch[batch.length - 1].endIndex
8789
+ };
8521
8790
  const extractionCtx = buildExtractionContext(extraction, range);
8522
8791
  const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0, TRUNC.OPEN_LOOP_SUMMARY) + (d.userResponse ? " \u2192 " + d.userResponse.slice(0, TRUNC.DECISION_DETAIL) : ""));
8523
8792
  const decisionCtx = activeDecisions.length ? `
@@ -8548,8 +8817,16 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
8548
8817
  const resp = await trackedComplete("batch", model, {
8549
8818
  systemPrompt: COMPACT_SYSTEM_PREFIX,
8550
8819
  messages: [
8551
- { role: "user", content: [{ type: "text", text: promptPrefix }], timestamp: Date.now() },
8552
- { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
8820
+ {
8821
+ role: "user",
8822
+ content: [{ type: "text", text: promptPrefix }],
8823
+ timestamp: Date.now()
8824
+ },
8825
+ {
8826
+ role: "user",
8827
+ content: [{ type: "text", text: dynamicSuffix }],
8828
+ timestamp: Date.now()
8829
+ }
8553
8830
  ]
8554
8831
  }, {
8555
8832
  apiKey: auth.apiKey,
@@ -8600,29 +8877,56 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
8600
8877
  setCachedBatch(cacheKey, result);
8601
8878
  return result;
8602
8879
  }
8603
- async function assembleLLM(summaries, extraction, report, model, auth, budget, prevContext, signal, services, focus) {
8880
+ function deterministicFileEvidence(extraction, budgetTokens, continuity = null) {
8881
+ const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
8882
+ const readPaths = extraction.readFiles;
8883
+ const deletedPaths = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
8884
+ const evidence = buildSummaryPathEvidence([...modifiedPaths, ...readPaths, ...deletedPaths], budgetTokens);
8885
+ return {
8886
+ modified: modifiedPaths.map((path12) => evidence.get(path12)).filter((path12) => Boolean(path12)),
8887
+ read: readPaths.map((path12) => evidence.get(path12)).filter((path12) => Boolean(path12)),
8888
+ deleted: deletedPaths.map((path12) => evidence.get(path12)).filter((path12) => Boolean(path12))
8889
+ };
8890
+ }
8891
+ async function assembleLLM(summaries, extraction, report, model, auth, budget, prevContext, signal, services, focus, continuity = null) {
8604
8892
  const pp = preProcessSummaries(summaries, budget, focus);
8605
- const detModified = extraction.modifiedFiles.map((f) => f.path);
8606
- const detRead = extraction.readFiles;
8607
- const detDeleted = extraction.deletedFiles;
8893
+ const files = deterministicFileEvidence(extraction, budget, continuity);
8894
+ const detModified = files.modified;
8895
+ const detRead = files.read;
8896
+ const detDeleted = files.deleted;
8608
8897
  const explorationCtx = report ? buildExplorationContext(report) : "";
8609
8898
  const dynamicSuffix = ASSEMBLY_PROMPT_SUFFIX.replace("{DECISIONS}", pp.decisions.join("; ") || "None").replace("{MODIFIED}", detModified.join(", ") || "None").replace("{READ}", detRead.join(", ") || "None").replace("{DELETED}", detDeleted.join(", ") || "None").replace("{EXPLORATION_CONTEXT}", explorationCtx).replace("{PREV_CONTEXT}", prevContext).replace("{SUMMARIES}", pp.text);
8610
8899
  const resp = await trackedComplete("assemble", model, {
8611
8900
  systemPrompt: COMPACT_SYSTEM_PREFIX,
8612
8901
  messages: [
8613
- { role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }], timestamp: Date.now() },
8614
- { role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
8902
+ {
8903
+ role: "user",
8904
+ content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }],
8905
+ timestamp: Date.now()
8906
+ },
8907
+ {
8908
+ role: "user",
8909
+ content: [{ type: "text", text: dynamicSuffix }],
8910
+ timestamp: Date.now()
8911
+ }
8615
8912
  ]
8616
- }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens), signal }, services);
8913
+ }, {
8914
+ apiKey: auth.apiKey,
8915
+ headers: auth.headers,
8916
+ maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens),
8917
+ signal
8918
+ }, services);
8617
8919
  return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
8618
8920
  `).trim();
8619
8921
  }
8620
- function assembleFallback(summaries, extraction, steering = {}) {
8922
+ function assembleFallback(summaries, extraction, steering = {}, budgetTokens = 6000, continuity = null) {
8621
8923
  const safe = (value, max = TRUNC.PREVIEW_MID) => summaryEvidenceLine(value, max);
8622
- const detModified = extraction.modifiedFiles.map((f) => safe(f.path)).filter(Boolean);
8623
- const detRead = extraction.readFiles.map((file) => safe(file)).filter(Boolean);
8624
- const detDeleted = extraction.deletedFiles.map((file) => safe(file)).filter(Boolean);
8924
+ const files = deterministicFileEvidence(extraction, budgetTokens, continuity);
8925
+ const detModified = files.modified;
8926
+ const detRead = files.read;
8927
+ const detDeleted = files.deleted;
8625
8928
  const unresolved = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.PREVIEW)).filter(Boolean);
8929
+ const resolved = extraction.errors.filter((error2) => error2.resolved).slice(-5).map((error2) => safe(error2.message, TRUNC.PREVIEW)).filter(Boolean);
8626
8930
  const constraints = extraction.constraints.map((item) => "- [" + item.category + "] " + safe(item.text)).filter((line) => !line.endsWith("] "));
8627
8931
  if (steering.focus?.trim())
8628
8932
  constraints.push("- [focus] Preserve detail about: " + safe(steering.focus, TRUNC.CONSTRAINT_TEXT));
@@ -8637,12 +8941,13 @@ function assembleFallback(summaries, extraction, steering = {}) {
8637
8941
  if (!inProgress.length) {
8638
8942
  inProgress.push(...extraction.lastUserMessages.slice(-3).map((message) => safe(message, TRUNC.PREVIEW)).filter(Boolean).map((message) => "- [ ] " + message));
8639
8943
  }
8640
- inProgress.push(...detModified.map((file) => "- [ ] Continue work in " + file));
8944
+ inProgress.push(...detModified.slice(-5).map((file) => "- [ ] Continue work in " + file));
8641
8945
  const next = safe(extraction.lastUserMessages.at(-1) ?? extraction.timeline.at(-1)?.summary ?? "", TRUNC.PREVIEW) || "Continue from the latest preserved context.";
8642
8946
  const goal = safe(extraction.mainGoal ?? "", TRUNC.DETAIL) || "Continue the current task.";
8643
8947
  const overflow = Object.entries(extraction.evidenceOverflow ?? {}).filter(([, count]) => typeof count === "number" && count > 0).map(([kind, count]) => "- Safety bound omitted " + count + " older " + kind + " item(s) from the human summary.");
8644
8948
  const critical = [
8645
8949
  ...unresolved.map((error2) => "- Unresolved error: " + safe(error2, TRUNC.TOPIC_LABEL)),
8950
+ ...resolved.map((error2) => "- Resolved error: " + safe(error2, TRUNC.TOPIC_LABEL)),
8646
8951
  ...overflow
8647
8952
  ];
8648
8953
  return [
@@ -8787,7 +9092,7 @@ async function summarizeConversation(rc) {
8787
9092
  phaseName: "Synthesize",
8788
9093
  detail: "Building a deterministic continuation summary \xB7 no LLM call"
8789
9094
  });
8790
- const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote });
9095
+ const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8791
9096
  setCachedSynthesis(cacheKey, {
8792
9097
  finalSummary: finalSummary2,
8793
9098
  method: "heuristic",
@@ -8818,7 +9123,7 @@ async function summarizeConversation(rc) {
8818
9123
  rc.vlog("Tier=" + rc.tier + " | convTokens=" + rc.convTokens + " | singlePassMax=" + singlePassMaxTokens);
8819
9124
  let finalSummary;
8820
9125
  let method;
8821
- let summaries = [];
9126
+ const summaries = [];
8822
9127
  let explorationReport = null;
8823
9128
  let explorationRounds = 0;
8824
9129
  let chunkCount = 0;
@@ -8839,7 +9144,7 @@ async function summarizeConversation(rc) {
8839
9144
  phaseName: "Synthesize",
8840
9145
  detail: "Summary route unavailable \xB7 building a deterministic summary"
8841
9146
  });
8842
- finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote });
9147
+ finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8843
9148
  method = "heuristic";
8844
9149
  } else if (rc.convTokens < singlePassMaxTokens) {
8845
9150
  showProgressOverlay(rc.ctx, {
@@ -8859,7 +9164,7 @@ async function summarizeConversation(rc) {
8859
9164
  generationFallbacks.push("single-pass generation failed");
8860
9165
  debugError("Single-pass synthesis used deterministic fallback", err);
8861
9166
  rc.notify("Single-pass generation stopped \xB7 using deterministic fallback", "info");
8862
- finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote });
9167
+ finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8863
9168
  method = "heuristic";
8864
9169
  }
8865
9170
  } else {
@@ -8876,7 +9181,10 @@ async function summarizeConversation(rc) {
8876
9181
  });
8877
9182
  try {
8878
9183
  const segAuth = await resolveStageAuth(rc, "explore");
8879
- const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [rc.userNote, rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined].filter(Boolean).join(`
9184
+ const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [
9185
+ rc.userNote,
9186
+ rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined
9187
+ ].filter(Boolean).join(`
8880
9188
  `) || undefined, rc.cancellation.signal, MAX_EXPLORATION_ROUNDS, rc.notify, rc.services);
8881
9189
  explorationReport = expResult.report;
8882
9190
  explorationRounds = expResult.rounds;
@@ -9050,7 +9358,7 @@ async function summarizeConversation(rc) {
9050
9358
  totalBatches: batches.length
9051
9359
  });
9052
9360
  try {
9053
- 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);
9361
+ 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);
9054
9362
  if (r?.startsWith("##"))
9055
9363
  finalSummary = r;
9056
9364
  else
@@ -9059,7 +9367,7 @@ async function summarizeConversation(rc) {
9059
9367
  cacheable = false;
9060
9368
  generationFallbacks.push("assembly generation failed");
9061
9369
  debugError("Assembly used deterministic fallback", err);
9062
- finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote });
9370
+ finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
9063
9371
  }
9064
9372
  method = "eesv";
9065
9373
  }
@@ -9092,6 +9400,76 @@ async function summarizeConversation(rc) {
9092
9400
  // src/phases/verify.ts
9093
9401
  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;
9094
9402
  var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
9403
+ var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
9404
+ var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
9405
+ var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
9406
+ function noneBlockerLineIndexes(lines) {
9407
+ const indexes = new Set;
9408
+ const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
9409
+ for (const item of nonEmpty) {
9410
+ if (BULLET_NONE_BLOCKER_RE.test(item.text))
9411
+ indexes.add(item.index);
9412
+ }
9413
+ if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
9414
+ indexes.add(nonEmpty[0].index);
9415
+ }
9416
+ return indexes;
9417
+ }
9418
+ function collectListedPaths(body, expectedPaths) {
9419
+ const values = new Set;
9420
+ const encodedValues = new Set;
9421
+ for (const line of body.split(`
9422
+ `)) {
9423
+ const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
9424
+ if (!raw)
9425
+ continue;
9426
+ if (raw.startsWith('"')) {
9427
+ try {
9428
+ const decoded = JSON.parse(raw);
9429
+ if (typeof decoded === "string") {
9430
+ values.add(decoded);
9431
+ encodedValues.add(decoded);
9432
+ continue;
9433
+ }
9434
+ } catch {}
9435
+ }
9436
+ values.add(raw);
9437
+ if (expectedPaths.has(raw))
9438
+ continue;
9439
+ const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
9440
+ const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
9441
+ if (expectedPaths.has(unchecked))
9442
+ values.add(unchecked);
9443
+ }
9444
+ return {
9445
+ values,
9446
+ encodedValues,
9447
+ normalizedValues: new Set(Array.from(values, normalizePath))
9448
+ };
9449
+ }
9450
+ function decodePathDisplay(display) {
9451
+ try {
9452
+ const decoded = JSON.parse(display);
9453
+ return typeof decoded === "string" ? decoded : display;
9454
+ } catch {
9455
+ return display;
9456
+ }
9457
+ }
9458
+ function hasListedPath(listed, file, display, normalizedOwners) {
9459
+ const decodedDisplay = decodePathDisplay(display);
9460
+ if (listed.encodedValues.has(decodedDisplay))
9461
+ return true;
9462
+ if (PATH_PLACEHOLDER_RE.test(file))
9463
+ return false;
9464
+ if (listed.values.has(file))
9465
+ return true;
9466
+ for (const candidate of [file, decodedDisplay]) {
9467
+ const normalized = normalizePath(candidate);
9468
+ if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
9469
+ return true;
9470
+ }
9471
+ return false;
9472
+ }
9095
9473
  function outcomeClaims(summary) {
9096
9474
  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("#")).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);
9097
9475
  }
@@ -9110,6 +9488,28 @@ function classifyOutcomeClaim(claim) {
9110
9488
  return "generic";
9111
9489
  }
9112
9490
  var successfulToolEvidenceCache = new WeakMap;
9491
+ var sourceTextCache = new WeakMap;
9492
+ function sourceSupportsFileReference(ref, messages) {
9493
+ let texts = sourceTextCache.get(messages);
9494
+ if (!texts) {
9495
+ texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
9496
+ sourceTextCache.set(messages, texts);
9497
+ }
9498
+ const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
9499
+ if (!needle)
9500
+ return false;
9501
+ for (const text of texts) {
9502
+ let index = text.indexOf(needle);
9503
+ while (index >= 0) {
9504
+ const before = text[index - 1] ?? "";
9505
+ const after = text[index + needle.length] ?? "";
9506
+ if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
9507
+ return true;
9508
+ index = text.indexOf(needle, index + 1);
9509
+ }
9510
+ }
9511
+ return false;
9512
+ }
9113
9513
  function successfulToolEvidence(messages) {
9114
9514
  const cached = successfulToolEvidenceCache.get(messages);
9115
9515
  if (cached)
@@ -9293,8 +9693,72 @@ var SEMANTIC_STOP = new Set([
9293
9693
  "de\u011Fil",
9294
9694
  "olmadan"
9295
9695
  ]);
9696
+ var TR_SUFFIXES = [
9697
+ "lar\u0131",
9698
+ "leri",
9699
+ "\u0131n\u0131n",
9700
+ "inin",
9701
+ "unun",
9702
+ "\xFCn\xFCn",
9703
+ "\u0131nda",
9704
+ "inde",
9705
+ "unda",
9706
+ "\xFCnde",
9707
+ "m\u0131\u015F",
9708
+ "mi\u015F",
9709
+ "mu\u015F",
9710
+ "m\xFC\u015F",
9711
+ "lar",
9712
+ "ler",
9713
+ "\u0131n\u0131",
9714
+ "ini",
9715
+ "unu",
9716
+ "\xFCn\xFC",
9717
+ "\u0131na",
9718
+ "ine",
9719
+ "una",
9720
+ "\xFCne",
9721
+ "dan",
9722
+ "den",
9723
+ "tan",
9724
+ "ten",
9725
+ "d\u0131r",
9726
+ "dir",
9727
+ "dur",
9728
+ "d\xFCr",
9729
+ "t\u0131r",
9730
+ "tir",
9731
+ "tur",
9732
+ "t\xFCr",
9733
+ "yor",
9734
+ "mak",
9735
+ "mek",
9736
+ "da",
9737
+ "de",
9738
+ "ta",
9739
+ "te",
9740
+ "d\u0131",
9741
+ "di",
9742
+ "du",
9743
+ "d\xFC",
9744
+ "t\u0131",
9745
+ "ti",
9746
+ "tu",
9747
+ "t\xFC",
9748
+ "\u0131n",
9749
+ "in",
9750
+ "un",
9751
+ "\xFCn",
9752
+ "sa",
9753
+ "se"
9754
+ ];
9296
9755
  function stemToken(token) {
9297
9756
  const lower = token.toLocaleLowerCase();
9757
+ for (const suffix of TR_SUFFIXES) {
9758
+ if (lower.length >= 4 + suffix.length && lower.endsWith(suffix)) {
9759
+ return lower.slice(0, -suffix.length);
9760
+ }
9761
+ }
9298
9762
  if (lower.length > 6 && lower.endsWith("ing"))
9299
9763
  return lower.slice(0, -3);
9300
9764
  if (lower.length > 5 && lower.endsWith("ed"))
@@ -9409,7 +9873,7 @@ function repairSummaryDeterministically(summary, result, extraction, continuity
9409
9873
  const patchable = result.gaps.filter(isDeterministicallyPatchable);
9410
9874
  if (!patchable.length)
9411
9875
  break;
9412
- const next = patchDeterministic(summary, patchable, extraction, continuity);
9876
+ const next = patchDeterministic(summary, patchable, extraction, continuity, evidence);
9413
9877
  if (next === summary)
9414
9878
  break;
9415
9879
  for (const gap of patchable) {
@@ -9442,11 +9906,15 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9442
9906
  };
9443
9907
  const unresolvedEvidence = uniqueByText([
9444
9908
  ...extraction.errors.filter((error2) => !error2.resolved).map((error2) => ({ message: error2.message })),
9445
- ...(continuity?.unresolvedErrors ?? []).map((error2) => ({ message: error2.message }))
9909
+ ...(continuity?.unresolvedErrors ?? []).map((error2) => ({
9910
+ message: error2.message
9911
+ }))
9446
9912
  ], (item) => item.message);
9447
9913
  const resolvedEvidence = uniqueByText([
9448
9914
  ...extraction.errors.filter((error2) => error2.resolved).map((error2) => ({ message: error2.message })),
9449
- ...(continuity?.resolvedErrors ?? []).map((error2) => ({ message: error2.message }))
9915
+ ...(continuity?.resolvedErrors ?? []).map((error2) => ({
9916
+ message: error2.message
9917
+ }))
9450
9918
  ], (item) => item.message).slice(-5);
9451
9919
  const steeringConstraints = [
9452
9920
  evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
@@ -9473,40 +9941,64 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9473
9941
  score -= req.penalty;
9474
9942
  }
9475
9943
  }
9476
- const listedPaths = (kind) => new Set((findSection(parsed, kind)?.body ?? "").split(`
9477
- `).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).map((line) => line.startsWith("`") && line.endsWith("`") ? line.slice(1, -1) : line).filter((line) => line.length > 0 && !/^none(?: recorded)?[.!]?$/i.test(line)).map(normalizePath));
9478
9944
  const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
9945
+ const readPaths = extraction.readFiles;
9946
+ const deletedEvidence = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
9947
+ const requiredPaths = [...modifiedPaths, ...readPaths, ...deletedEvidence];
9948
+ const expectedPathSet = new Set(requiredPaths);
9949
+ const pathEvidence = buildSummaryPathEvidence(requiredPaths, evidence.summaryBudgetTokens);
9950
+ const normalizedOwnerSets = new Map;
9951
+ for (const file of requiredPaths) {
9952
+ const display = pathEvidence.get(file);
9953
+ for (const candidate of [
9954
+ file,
9955
+ ...display ? [decodePathDisplay(display)] : []
9956
+ ]) {
9957
+ const normalized = normalizePath(candidate);
9958
+ const owners = normalizedOwnerSets.get(normalized) ?? new Set;
9959
+ owners.add(file);
9960
+ normalizedOwnerSets.set(normalized, owners);
9961
+ }
9962
+ }
9963
+ const normalizedOwners = new Map(Array.from(normalizedOwnerSets, ([path12, owners]) => [path12, owners.size]));
9964
+ const listedPaths = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expectedPathSet);
9479
9965
  const modifiedListed = listedPaths("files-modified");
9480
9966
  const readListed = listedPaths("files-read");
9481
9967
  const deletedListed = listedPaths("files-deleted");
9482
9968
  for (const file of modifiedPaths) {
9483
- if (!modifiedListed.has(normalizePath(file)))
9969
+ const display = pathEvidence.get(file);
9970
+ if (display && !hasListedPath(modifiedListed, file, display, normalizedOwners)) {
9484
9971
  gaps.push({ kind: "missing-file", path: file });
9972
+ }
9485
9973
  }
9486
- for (const file of extraction.readFiles) {
9487
- if (!readListed.has(normalizePath(file)))
9974
+ for (const file of readPaths) {
9975
+ const display = pathEvidence.get(file);
9976
+ if (display && !hasListedPath(readListed, file, display, normalizedOwners)) {
9488
9977
  gaps.push({ kind: "missing-read-file", path: file });
9978
+ }
9489
9979
  }
9490
- const deletedEvidence = Array.from(new Set([
9491
- ...extraction.deletedFiles,
9492
- ...continuity?.deletedFiles ?? []
9493
- ]));
9494
9980
  for (const file of deletedEvidence) {
9495
- if (!deletedListed.has(normalizePath(file)))
9981
+ const display = pathEvidence.get(file);
9982
+ if (display && !hasListedPath(deletedListed, file, display, normalizedOwners)) {
9496
9983
  gaps.push({ kind: "missing-deleted-file", path: file });
9984
+ }
9497
9985
  }
9498
9986
  score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
9499
9987
  for (const error2 of unresolvedEvidence) {
9500
- const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase();
9988
+ const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
9501
9989
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
9502
9990
  gaps.push({ kind: "missing-error", message: error2.message });
9503
9991
  score -= 5;
9504
9992
  }
9505
9993
  }
9506
9994
  for (const error2 of resolvedEvidence) {
9507
- const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase();
9995
+ const snippet = summaryEvidenceLine(error2.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
9508
9996
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
9509
- gaps.push({ kind: "missing-error", message: error2.message, resolved: true });
9997
+ gaps.push({
9998
+ kind: "missing-error",
9999
+ message: error2.message,
10000
+ resolved: true
10001
+ });
9510
10002
  score -= 2;
9511
10003
  }
9512
10004
  }
@@ -9521,7 +10013,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9521
10013
  score -= 8;
9522
10014
  }
9523
10015
  if (hasSemanticContradiction(constraint.text, constraintTarget)) {
9524
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET) });
10016
+ gaps.push({
10017
+ kind: "inconsistency",
10018
+ detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
10019
+ });
9525
10020
  score -= 20;
9526
10021
  }
9527
10022
  }
@@ -9532,32 +10027,52 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9532
10027
  score -= 12;
9533
10028
  }
9534
10029
  if (hasSemanticContradiction(goalEvidence, goalTarget)) {
9535
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: goal polarity or condition changed" });
10030
+ gaps.push({
10031
+ kind: "inconsistency",
10032
+ detail: "semantic-contradiction: goal polarity or condition changed"
10033
+ });
9536
10034
  score -= 20;
9537
10035
  }
9538
10036
  }
9539
- const groundedEvidenceFiles = [
10037
+ const groundedEvidence = [
9540
10038
  ...unresolvedEvidence.map((item) => item.message),
10039
+ ...resolvedEvidence.map((item) => item.message),
9541
10040
  ...constraintEvidence.map((item) => item.text),
9542
10041
  ...decisionEvidence.map((item) => item.summary),
9543
10042
  ...goalEvidence ? [goalEvidence] : [],
10043
+ ...extraction.lastUserMessages,
10044
+ ...extraction.timeline.map((item) => item.summary),
10045
+ ...extraction.topics.map((item) => item.primaryFile ?? ""),
9544
10046
  ...continuity?.openLoops.map((item) => item.summary) ?? [],
9545
10047
  ...continuity?.criticalContext ?? []
9546
- ].flatMap(extractFileRefs);
9547
- const knownFiles = Array.from(new Set([
10048
+ ];
10049
+ const groundedEvidenceFiles = groundedEvidence.flatMap((value) => [
10050
+ value,
10051
+ summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
10052
+ summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
10053
+ summaryEvidenceLine(value, TRUNC.PREVIEW),
10054
+ summaryEvidenceLine(value, TRUNC.MESSAGE)
10055
+ ]).flatMap(extractFileRefs);
10056
+ const renderedPathEvidence = Array.from(pathEvidence.values()).flatMap((line) => [
10057
+ decodePathDisplay(line),
10058
+ line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
10059
+ ]);
10060
+ const rawKnownFiles = Array.from(new Set([
9548
10061
  ...modifiedPaths,
9549
- ...extraction.readFiles,
9550
- ...extraction.deletedFiles,
10062
+ ...readPaths,
10063
+ ...deletedEvidence,
9551
10064
  ...extraction.referencedFiles ?? [],
9552
10065
  ...groundedEvidenceFiles,
10066
+ ...renderedPathEvidence,
10067
+ ...renderedPathEvidence.flatMap(extractFileRefs),
9553
10068
  ...continuity?.modifiedFiles ?? [],
9554
10069
  ...continuity?.readFiles ?? [],
9555
- ...continuity?.deletedFiles ?? [],
9556
10070
  ...(continuity?.unresolvedErrors ?? []).flatMap((error2) => error2.files),
9557
10071
  ...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
9558
10072
  ]));
9559
10073
  for (const ref of new Set(extractFileRefs(summary))) {
9560
- if (!isKnownPathReference(ref, knownFiles)) {
10074
+ const grounded = isKnownPathReference(ref, rawKnownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
10075
+ if (!grounded) {
9561
10076
  gaps.push({ kind: "fabricated-file", ref });
9562
10077
  score -= 4;
9563
10078
  }
@@ -9566,8 +10081,12 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9566
10081
  if (progressSection) {
9567
10082
  const doneSection = progressSection.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
9568
10083
  const blockedSection = progressSection.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
9569
- if (unresolvedEvidence.length > 0 && /(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?\s*$/im.test(blockedSection)) {
9570
- gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
10084
+ const blockedLines = blockedSection.split(/\r?\n/).slice(1);
10085
+ if (unresolvedEvidence.length > 0 && noneBlockerLineIndexes(blockedLines).size > 0) {
10086
+ gaps.push({
10087
+ kind: "inconsistency",
10088
+ detail: "blocked-none: Blocked says none despite unresolved errors"
10089
+ });
9571
10090
  score -= 12;
9572
10091
  }
9573
10092
  const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
@@ -9581,7 +10100,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9581
10100
  return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
9582
10101
  });
9583
10102
  if (unresolved) {
9584
- gaps.push({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
10103
+ gaps.push({
10104
+ kind: "inconsistency",
10105
+ detail: file.path + " marked Done but has unresolved error"
10106
+ });
9585
10107
  score -= 5;
9586
10108
  }
9587
10109
  }
@@ -9593,7 +10115,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9593
10115
  score -= 8;
9594
10116
  }
9595
10117
  if (hasSemanticContradiction(decision.summary, decisionBody)) {
9596
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET) });
10118
+ gaps.push({
10119
+ kind: "inconsistency",
10120
+ detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
10121
+ });
9597
10122
  score -= 20;
9598
10123
  }
9599
10124
  }
@@ -9614,8 +10139,17 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9614
10139
  const finalScore = Math.max(0, score);
9615
10140
  return { ok: gaps.length === 0 && finalScore >= 85, gaps, score: finalScore };
9616
10141
  }
9617
- function patchDeterministic(summary, gaps, extraction, continuity = null) {
10142
+ function patchDeterministic(summary, gaps, extraction, continuity = null, evidence = {}) {
9618
10143
  let canonical = parseSummary(summary);
10144
+ const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
10145
+ const readPaths = extraction.readFiles;
10146
+ const deletedPaths = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
10147
+ const pathEvidence = buildSummaryPathEvidence([...modifiedPaths, ...readPaths, ...deletedPaths], evidence.summaryBudgetTokens);
10148
+ const replaceFileSection = (kind, paths) => {
10149
+ const body = paths.map((path12) => "- " + (pathEvidence.get(path12) ?? JSON.stringify(path12))).join(`
10150
+ `);
10151
+ canonical = upsertSection(canonical, kind, body || "- None recorded.");
10152
+ };
9619
10153
  const safe = (value, max = TRUNC.MESSAGE) => summaryEvidenceLine(value, max);
9620
10154
  const unresolvedMessages = Array.from(new Set([
9621
10155
  ...extraction.errors.filter((error2) => !error2.resolved).map((error2) => error2.message),
@@ -9630,9 +10164,24 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
9630
10164
  const progress = findSection(canonical, "progress");
9631
10165
  if (!progress || !blockedItems.length)
9632
10166
  return;
9633
- const body = progress.body.replace(/(###\s*Blocked\s*\n)(?:-\s*(?:none|none recorded|no blockers?|yok)[.!]?\s*)/i, "$1" + blockedItems.join(`
10167
+ const lines = progress.body.split(/\r?\n/);
10168
+ const start = lines.findIndex((line) => /^###\s*Blocked\s*$/i.test(line.trim()));
10169
+ if (start < 0)
10170
+ return;
10171
+ let end = lines.findIndex((line, index) => index > start && /^###\s+/.test(line.trim()));
10172
+ if (end < 0)
10173
+ end = lines.length;
10174
+ const existing = lines.slice(start + 1, end);
10175
+ const noneIndexes = noneBlockerLineIndexes(existing);
10176
+ if (!noneIndexes.size)
10177
+ return;
10178
+ const replacement = Array.from(new Set([
10179
+ ...blockedItems,
10180
+ ...existing.filter((line, index) => line.trim() && !noneIndexes.has(index))
10181
+ ]));
10182
+ lines.splice(start + 1, end - start - 1, ...replacement);
10183
+ canonical = upsertSection(canonical, "progress", lines.join(`
9634
10184
  `));
9635
- canonical = upsertSection(canonical, "progress", body);
9636
10185
  };
9637
10186
  for (const gap of gaps) {
9638
10187
  switch (gap.kind) {
@@ -9648,20 +10197,23 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
9648
10197
  ` + (blockedItems.join(`
9649
10198
  `) || "- None recorded."));
9650
10199
  } else if (gap.section === "critical-context") {
9651
- const critical = unresolvedMessages.map((message) => safe(message)).filter(Boolean).map((message) => "- Unresolved error: " + message);
10200
+ const critical = unresolvedMessages.flatMap((message) => {
10201
+ const text = safe(message);
10202
+ return text ? ["- Unresolved error: " + text] : [];
10203
+ });
9652
10204
  canonical = upsertSection(canonical, "critical-context", critical.join(`
9653
10205
  `) || "- None recorded.");
9654
10206
  }
9655
10207
  break;
9656
10208
  }
9657
10209
  case "missing-file":
9658
- canonical = appendToSection(canonical, "files-modified", "- " + safe(gap.path));
10210
+ replaceFileSection("files-modified", modifiedPaths);
9659
10211
  break;
9660
10212
  case "missing-read-file":
9661
- canonical = appendToSection(canonical, "files-read", "- " + safe(gap.path));
10213
+ replaceFileSection("files-read", readPaths);
9662
10214
  break;
9663
10215
  case "missing-deleted-file":
9664
- canonical = appendToSection(canonical, "files-deleted", "- " + safe(gap.path));
10216
+ replaceFileSection("files-deleted", deletedPaths);
9665
10217
  break;
9666
10218
  case "missing-error": {
9667
10219
  const existing = findSection(canonical, "critical-context")?.body.toLowerCase() ?? "";
@@ -9683,7 +10235,10 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
9683
10235
  case "missing-open-loops": {
9684
10236
  const current = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
9685
10237
  const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
9686
- const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({ priority: loop.priority, summary: safe(loop.summary, TRUNC.SNIPPET) })).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
10238
+ const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({
10239
+ priority: loop.priority,
10240
+ summary: safe(loop.summary, TRUNC.SNIPPET)
10241
+ })).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
9687
10242
  const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
9688
10243
  `);
9689
10244
  canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
@@ -9754,7 +10309,13 @@ Return the COMPLETE corrected summary in the same format.`;
9754
10309
  const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
9755
10310
  const response = await trackedComplete("patch", model, {
9756
10311
  systemPrompt: COMPACT_SYSTEM_PREFIX,
9757
- messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }], timestamp: Date.now() }]
10312
+ messages: [
10313
+ {
10314
+ role: "user",
10315
+ content: [{ type: "text", text: patchPrompt }],
10316
+ timestamp: Date.now()
10317
+ }
10318
+ ]
9758
10319
  }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
9759
10320
  const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
9760
10321
  `).trim();
@@ -9762,7 +10323,10 @@ Return the COMPLETE corrected summary in the same format.`;
9762
10323
  return summary;
9763
10324
  const originalSections = parseSummary(summary).sections;
9764
10325
  const patchedSections = parseSummary(patched).sections;
9765
- const patchedBodies = new Map(patchedSections.map((section) => [sectionIdentity(section), section.body.trim()]));
10326
+ const patchedBodies = new Map(patchedSections.map((section) => [
10327
+ sectionIdentity(section),
10328
+ section.body.trim()
10329
+ ]));
9766
10330
  const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
9767
10331
  return preserved ? patched : summary;
9768
10332
  } catch (error2) {
@@ -9775,9 +10339,11 @@ Return the COMPLETE corrected summary in the same format.`;
9775
10339
  async function verifyAndPatch(rc) {
9776
10340
  const extraction = rc.extraction;
9777
10341
  let summary = rc.finalSummary;
10342
+ const summaryBudgetTokens = rc.profileCfg?.summaryBudgetTokens ?? 6000;
9778
10343
  const evidence = {
9779
10344
  sourceMessages: rc.llmMessages,
9780
- steering: { focus: rc.focus, note: rc.userNote }
10345
+ steering: { focus: rc.focus, note: rc.userNote },
10346
+ summaryBudgetTokens
9781
10347
  };
9782
10348
  showProgressOverlay(rc.ctx, {
9783
10349
  phase: 4,
@@ -9840,7 +10406,7 @@ async function verifyAndPatch(rc) {
9840
10406
  detail: "Trying the deterministic safety summary",
9841
10407
  explorationRounds: rc.explorationRounds
9842
10408
  });
9843
- let deterministic = assembleFallback([], extraction, evidence.steering);
10409
+ let deterministic = assembleFallback([], extraction, evidence.steering, summaryBudgetTokens, rc.previousState);
9844
10410
  let deterministicVerification = verifySummary(deterministic, extraction, rc.previousState, evidence);
9845
10411
  const repaired = repairSummaryDeterministically(deterministic, deterministicVerification, extraction, rc.previousState, evidence);
9846
10412
  deterministic = repaired.summary;
@@ -10062,7 +10628,7 @@ function buildState(rc) {
10062
10628
  }
10063
10629
 
10064
10630
  // src/infra/context-graph.ts
10065
- import { createHash as createHash3 } from "crypto";
10631
+ import { createHash as createHash4 } from "crypto";
10066
10632
  import fs9 from "fs";
10067
10633
  import path13 from "path";
10068
10634
  import { createRequire } from "module";
@@ -10193,7 +10759,7 @@ function openDatabase() {
10193
10759
  return db;
10194
10760
  }
10195
10761
  function stableId(...parts) {
10196
- return "cg-" + createHash3("sha256").update(parts.join("\x00")).digest("hex").slice(0, 24);
10762
+ return "cg-" + createHash4("sha256").update(parts.join("\x00")).digest("hex").slice(0, 24);
10197
10763
  }
10198
10764
  function factKey(text) {
10199
10765
  return normalizeFactKey(text) || text.trim().toLowerCase();