pi-smart-compact 9.2.1 → 9.3.0

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