pi-smart-compact 8.0.2 → 8.0.3

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
@@ -130,7 +130,7 @@ var init_mode_policy = __esm(() => {
130
130
  });
131
131
 
132
132
  // src/constants.ts
133
- var VERSION = "8.0.2", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
133
+ var VERSION = "8.0.3", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
134
134
  {PREV_CONTEXT}
135
135
 
136
136
  {EXTRACTION_CONTEXT}
@@ -1314,6 +1314,12 @@ function isBenignSearchResult(tc, result) {
1314
1314
  const exit = result.match(/Command exited with code (\d+)\s*$/i)?.[1];
1315
1315
  return exit === undefined || exit === "1";
1316
1316
  }
1317
+ function hasCommandFailureSignal(text) {
1318
+ if (/Command exited with code [1-9]\d*\s*$/i.test(text))
1319
+ return true;
1320
+ const firstLine = text.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
1321
+ return LIKELY_ERROR_RE.test(firstLine) || /^(?:npm\s+error|fatal:|traceback\b)/i.test(firstLine);
1322
+ }
1317
1323
  function catalogErrors(msgs, _tcIdx) {
1318
1324
  const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
1319
1325
  const errors = [];
@@ -1330,7 +1336,7 @@ function catalogErrors(msgs, _tcIdx) {
1330
1336
  continue;
1331
1337
  }
1332
1338
  if (tc && classifyToolOperation(tc.arguments, tc.name) === "execute") {
1333
- if (LIKELY_ERROR_RE.test(text) && text.length < ERROR_SCAN_MAX_LEN) {
1339
+ if (hasCommandFailureSignal(text) && text.length < ERROR_SCAN_MAX_LEN) {
1334
1340
  errors.push({ index: i, tool: tc.name, message: text.slice(0, TRUNC.MESSAGE), retryAttempted: false, resolved: false });
1335
1341
  }
1336
1342
  }
@@ -3148,6 +3154,8 @@ function classifyTelemetryFailure(error, timedOut = false) {
3148
3154
  const text = (fields.name + " " + fields.code + " " + fields.message).toLowerCase();
3149
3155
  if (timedOut || /timeout|timed out|watchdog|deadline/.test(text))
3150
3156
  return "timeout";
3157
+ if (fields.name.toLowerCase() === "verificationgateerror")
3158
+ return "verification";
3151
3159
  if (/budgetexceeded|token budget|call budget|latency budget/.test(text))
3152
3160
  return "budget";
3153
3161
  if (fields.status === 429 || /rate.?limit|too many requests|quota/.test(text))
@@ -3160,6 +3168,8 @@ function classifyTelemetryFailure(error, timedOut = false) {
3160
3168
  return "cancelled";
3161
3169
  if (/native compaction|persist|write|rename|filesystem|sqlite|database/.test(text))
3162
3170
  return "persistence";
3171
+ if (/verificationgateerror|verification gate|verification.*(?:gap|summary)/.test(text))
3172
+ return "verification";
3163
3173
  if (/invalid|validation|schema|malformed|required/.test(text))
3164
3174
  return "validation";
3165
3175
  if (fields.status != null && fields.status >= 500 || /provider|api error|stream|network|fetch failed|socket/.test(text))
@@ -3292,6 +3302,7 @@ var init_telemetry = __esm(() => {
3292
3302
  "provider",
3293
3303
  "persistence",
3294
3304
  "validation",
3305
+ "verification",
3295
3306
  "internal"
3296
3307
  ]);
3297
3308
  });
@@ -3614,6 +3625,7 @@ function buildDashboardInsights(entries, damageEntries = [], options = {}) {
3614
3625
  "provider",
3615
3626
  "persistence",
3616
3627
  "validation",
3628
+ "verification",
3617
3629
  "internal"
3618
3630
  ]);
3619
3631
  for (const entry of entries) {
@@ -3704,6 +3716,9 @@ var init_summary_schema = __esm(() => {
3704
3716
  });
3705
3717
 
3706
3718
  // src/domain/summary-parse.ts
3719
+ function summaryEvidenceLine(value, maxLength) {
3720
+ return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
3721
+ }
3707
3722
  function mergeBodies(first, second) {
3708
3723
  const seen = new Set;
3709
3724
  return [first, second].filter(Boolean).flatMap((body) => body.split(`
@@ -4003,7 +4018,7 @@ function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, exi
4003
4018
  const haystack = normalizeFactKey(existing);
4004
4019
  const lines = ["## Continuity Ledger"];
4005
4020
  const add = (label, value) => {
4006
- const text = value.trim();
4021
+ const text = summaryEvidenceLine(value, TRUNC.MESSAGE);
4007
4022
  if (!text || haystack.includes(normalizeFactKey(text)))
4008
4023
  return;
4009
4024
  const line = "- " + label + ": " + text;
@@ -4031,8 +4046,9 @@ function injectOpenLoopsSection(summary, openLoops) {
4031
4046
  return summary;
4032
4047
  const body = openLoops.map((l) => {
4033
4048
  const prio = l.priority === "critical" || l.priority === "high" ? "[" + l.priority + "] " : "";
4034
- const files = l.files.length ? " \u2014 " + l.files.join(", ") : "";
4035
- return "- " + prio + l.summary + files;
4049
+ const files = l.files.map((file) => summaryEvidenceLine(file, TRUNC.MESSAGE)).filter(Boolean);
4050
+ const suffix = files.length ? " \u2014 " + files.join(", ") : "";
4051
+ return "- " + prio + summaryEvidenceLine(l.summary, TRUNC.OPEN_LOOP_SUMMARY) + suffix;
4036
4052
  }).join(`
4037
4053
  `);
4038
4054
  const parsed = parseSummary(summary);
@@ -4081,32 +4097,33 @@ function hasDeltaChanges(delta) {
4081
4097
  }
4082
4098
  function formatDeltaSection(delta) {
4083
4099
  const lines = ["## Changes Since Last Compaction", ""];
4100
+ const safe = (value, max) => summaryEvidenceLine(value, max);
4084
4101
  if (delta.goalChanged) {
4085
- lines.push("- **Goal shifted**: " + (delta.previousGoal ?? "?") + " \u2192 see current goal above");
4102
+ lines.push("- **Goal shifted**: " + safe(delta.previousGoal ?? "?", TRUNC.MESSAGE) + " \u2192 see current goal above");
4086
4103
  }
4087
4104
  if (delta.resolvedLoops.length) {
4088
- lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0, TRUNC.DECISION_DETAIL) + "~~").join(", "));
4105
+ lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + safe(s, TRUNC.DECISION_DETAIL) + "~~").join(", "));
4089
4106
  }
4090
4107
  if (delta.persistentLoops.length) {
4091
- lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
4108
+ lines.push("- **Still open**: " + delta.persistentLoops.map((s) => safe(s, TRUNC.DECISION_DETAIL)).join("; "));
4092
4109
  }
4093
4110
  if (delta.newLoops.length) {
4094
- lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
4111
+ lines.push("- **New loops**: " + delta.newLoops.map((s) => safe(s, TRUNC.DECISION_DETAIL)).join("; "));
4095
4112
  }
4096
4113
  if (delta.newDecisions.length) {
4097
- lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, TRUNC.SNIPPET)).join("; "));
4114
+ lines.push("- **New decisions**: " + delta.newDecisions.map((s) => safe(s, TRUNC.SNIPPET)).join("; "));
4098
4115
  }
4099
4116
  if (delta.removedDecisions.length) {
4100
- lines.push("- **Removed decisions**: " + delta.removedDecisions.map((s) => "~~" + s.slice(0, TRUNC.SNIPPET) + "~~").join("; "));
4117
+ lines.push("- **Removed decisions**: " + delta.removedDecisions.map((s) => "~~" + safe(s, TRUNC.SNIPPET) + "~~").join("; "));
4101
4118
  }
4102
4119
  if (delta.resolvedErrors.length) {
4103
- lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
4120
+ lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => safe(s, TRUNC.DECISION_DETAIL)).join("; "));
4104
4121
  }
4105
4122
  if (delta.newErrors.length) {
4106
- lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
4123
+ lines.push("- **New errors**: " + delta.newErrors.map((s) => safe(s, TRUNC.DECISION_DETAIL)).join("; "));
4107
4124
  }
4108
4125
  if (delta.newModifiedFiles.length) {
4109
- lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
4126
+ lines.push("- **New files touched**: " + delta.newModifiedFiles.map((file) => safe(file, TRUNC.MESSAGE)).join(", "));
4110
4127
  }
4111
4128
  lines.push("");
4112
4129
  return lines.join(`
@@ -4128,7 +4145,7 @@ function ensurePinnedPaths(summary, pinned) {
4128
4145
  if (!pinned.length)
4129
4146
  return summary;
4130
4147
  const lower = summary.toLowerCase();
4131
- const missing = pinned.filter((p) => p && p.trim().length > 0 && !lower.includes(p.toLowerCase()));
4148
+ const missing = pinned.map((path7) => summaryEvidenceLine(path7, TRUNC.MESSAGE)).filter((path7) => path7 && !lower.includes(path7.toLowerCase()));
4132
4149
  if (!missing.length)
4133
4150
  return summary;
4134
4151
  const parsed = parseSummary(summary);
@@ -5725,33 +5742,42 @@ async function prepareRun(rc) {
5725
5742
  // src/app/steps/window.ts
5726
5743
  init_mode_policy();
5727
5744
  init_helpers();
5745
+ init_constants();
5728
5746
  function resolveCompactionWindow(rc) {
5729
5747
  const usage = rc.ctx.getContextUsage();
5730
5748
  const totalTokens = usage?.tokens ?? 0;
5731
5749
  const manager = rc.ctx.sessionManager;
5732
5750
  const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
5733
5751
  const msgs = branch.filter((e) => e.type === "message" && e.message != null);
5734
- if (msgs.length < 3)
5752
+ if (msgs.length < 3) {
5753
+ if (rc.flags.force)
5754
+ rc.notify("Manual compaction skipped: fewer than 3 active messages are available.", "warning");
5735
5755
  return null;
5756
+ }
5736
5757
  const adaptiveKeepTokens = rc.ctx.model ? Math.min(rc.profileCfg.keepRecentTokens * 2, Math.max(rc.profileCfg.keepRecentTokens, rc.ctx.model.contextWindow * 0.04)) : rc.profileCfg.keepRecentTokens;
5737
5758
  const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
5738
5759
  const targetPercent = MODE_POLICIES[mode].targetContextPercent;
5739
5760
  const messageTokens = msgs.map((entry) => rc.estimator.message(entry.message));
5740
5761
  const allMessageTokens = messageTokens.reduce((sum, tokens) => sum + tokens, 0);
5741
- const fixedContextTokens = Math.max(0, totalTokens - allMessageTokens);
5762
+ const overflowedContext = !!rc.flags.overflowRecovery || !!rc.ctx.model && totalTokens > rc.ctx.model.contextWindow;
5763
+ const messageScale = totalTokens > 0 && allMessageTokens > 0 && (allMessageTokens > totalTokens || overflowedContext) ? totalTokens / allMessageTokens : 1;
5764
+ const fixedContextTokens = overflowedContext ? 0 : Math.max(0, totalTokens - allMessageTokens);
5742
5765
  const targetRetainedTokens = rc.ctx.model ? Math.max(0, rc.ctx.model.contextWindow * targetPercent / 100 - fixedContextTokens - rc.profileCfg.summaryBudgetTokens) : adaptiveKeepTokens;
5743
- const retentionBudget = Math.max(adaptiveKeepTokens, targetRetainedTokens);
5766
+ const retentionBudget = rc.flags.force ? adaptiveKeepTokens : Math.max(adaptiveKeepTokens, targetRetainedTokens);
5767
+ const rawAdaptiveKeepTokens = adaptiveKeepTokens / messageScale;
5768
+ const rawRetentionBudget = retentionBudget / messageScale;
5744
5769
  let accTokens = 0;
5745
5770
  let keepFrom = msgs.length - 1;
5746
5771
  for (let i = msgs.length - 1;i >= 0; i--) {
5747
5772
  const next = messageTokens[i];
5748
- if (accTokens >= adaptiveKeepTokens && accTokens + next > retentionBudget) {
5773
+ if (accTokens >= rawAdaptiveKeepTokens && accTokens + next > rawRetentionBudget) {
5749
5774
  keepFrom = i + 1;
5750
5775
  break;
5751
5776
  }
5752
5777
  accTokens += next;
5753
5778
  keepFrom = i;
5754
5779
  }
5780
+ const plannedKeepFrom = keepFrom;
5755
5781
  const recentUsers = msgs.map((entry, index) => ({ index, role: entry.message?.role })).filter((entry) => entry.role === "user");
5756
5782
  if (recentUsers.length) {
5757
5783
  const protectedUser = recentUsers[recentUsers.length >= 2 ? recentUsers.length - 2 : recentUsers.length - 1];
@@ -5759,19 +5785,42 @@ function resolveCompactionWindow(rc) {
5759
5785
  }
5760
5786
  keepFrom = smartKeepBoundary(msgs, keepFrom, branch);
5761
5787
  keepFrom = guardToolCallBoundary(msgs, keepFrom);
5762
- const rawCompactTokens = messageTokens.slice(0, keepFrom).reduce((sum, tokens) => sum + tokens, 0);
5763
- const rawRetainedTokens = messageTokens.slice(keepFrom).reduce((sum, tokens) => sum + tokens, 0);
5764
- const messageScale = totalTokens > 0 && allMessageTokens > totalTokens ? totalTokens / allMessageTokens : 1;
5765
- const compactTokens = Math.round(rawCompactTokens * messageScale);
5766
- accTokens = Math.round(rawRetainedTokens * messageScale);
5788
+ const recount = (from) => ({
5789
+ compact: Math.round(messageTokens.slice(0, from).reduce((sum, tokens) => sum + tokens, 0) * messageScale),
5790
+ retained: Math.round(messageTokens.slice(from).reduce((sum, tokens) => sum + tokens, 0) * messageScale)
5791
+ });
5792
+ let counts = recount(keepFrom);
5793
+ const softProtectionBudget = rc.flags.force ? Math.max(retentionBudget, adaptiveKeepTokens * 2) : retentionBudget;
5794
+ const protectionCeiling = fixedContextTokens + softProtectionBudget + rc.profileCfg.summaryBudgetTokens;
5795
+ if (overflowedContext && keepFrom < plannedKeepFrom && fixedContextTokens + counts.retained + rc.profileCfg.summaryBudgetTokens > protectionCeiling) {
5796
+ keepFrom = guardToolCallBoundary(msgs, plannedKeepFrom);
5797
+ counts = recount(keepFrom);
5798
+ rc.notify("Context exceeds the active model window. EESV will summarize through soft recent-turn/checkpoint protections while preserving complete tool-call pairs; native fallback would resend the oversized context.", "warning");
5799
+ }
5800
+ const compactTokens = counts.compact;
5801
+ accTokens = counts.retained;
5767
5802
  if (msgs[keepFrom]?.message?.role === "toolResult") {
5803
+ if (rc.flags.force)
5804
+ rc.notify("Manual compaction skipped: no safe boundary can separate the retained tool result from its tool call.", "warning");
5768
5805
  return null;
5769
5806
  }
5770
5807
  const toCompact = msgs.slice(0, keepFrom);
5771
- if (!toCompact.length)
5808
+ if (!toCompact.length) {
5809
+ if (rc.flags.force) {
5810
+ rc.notify("Manual compaction skipped: no eligible prefix remains after protecting recent user turns and tool-call boundaries. It may have been run again too soon.", "warning");
5811
+ }
5772
5812
  return null;
5813
+ }
5773
5814
  const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
5774
- if (!rc.flags.force && rc.ctx.model && totalTokens > 0 && rc.config.minContextPercent > 0) {
5815
+ if (rc.flags.force) {
5816
+ const lowYield = compactTokens <= rc.profileCfg.summaryBudgetTokens + MIN_TOKEN_THRESHOLD || totalTokens > 0 && compactTokens / totalTokens < 0.1;
5817
+ if (lowYield) {
5818
+ rc.notify("Low-yield manual compaction: only " + compactTokens.toLocaleString() + "t are eligible after preserving " + accTokens.toLocaleString() + "t of recent context. Continuing because you requested it; repeated compaction may lose nuance.", "warning");
5819
+ } else if (rc.config.minContextPercent > 0 && contextPercent < rc.config.minContextPercent) {
5820
+ rc.notify("Manual compaction override at " + Math.round(contextPercent) + "% (" + totalTokens.toLocaleString() + "t): compacting about " + compactTokens.toLocaleString() + "t while preserving " + accTokens.toLocaleString() + "t of recent context. Early compaction is lossy; verification remains fail-closed.", "warning");
5821
+ }
5822
+ }
5823
+ if (!rc.flags.force && !overflowedContext && rc.ctx.model && totalTokens > 0 && rc.config.minContextPercent > 0) {
5775
5824
  const projectedTokens = fixedContextTokens + accTokens + rc.profileCfg.summaryBudgetTokens;
5776
5825
  const targetTokens = rc.ctx.model.contextWindow * targetPercent / 100;
5777
5826
  if (projectedTokens > targetTokens) {
@@ -6032,7 +6081,7 @@ init_constants();
6032
6081
  init_helpers();
6033
6082
  function selectTier(rc) {
6034
6083
  const toolPercent = computeToolCharPercentage(rc.branch);
6035
- const tier = rc.flags.force ? rc.contextPercent >= 80 ? "full" : "light" : selectCompactionTier(rc.contextPercent, toolPercent, rc.totalTokens, MIN_TOKEN_THRESHOLD, rc.config.minContextPercent);
6084
+ const tier = rc.flags.overflowRecovery ? "full" : rc.flags.force ? rc.contextPercent >= 80 ? "full" : "light" : selectCompactionTier(rc.contextPercent, toolPercent, rc.totalTokens, MIN_TOKEN_THRESHOLD, rc.config.minContextPercent);
6036
6085
  if (tier === "none") {
6037
6086
  if (!rc.flags.autoTriggered) {
6038
6087
  rc.ctx.ui.notify("Context OK (" + Math.round(rc.contextPercent) + "%). pi-toolkit manages context well.", "info");
@@ -6853,6 +6902,7 @@ function getCachedBatchClone(value) {
6853
6902
  }
6854
6903
 
6855
6904
  // src/phases/synthesize.ts
6905
+ init_summary_parse();
6856
6906
  function boundedToolArgs(value, depth = 0) {
6857
6907
  if (typeof value === "string")
6858
6908
  return value.length > TRUNC.DETAIL ? value.slice(0, TRUNC.DETAIL) + "\u2026" : value;
@@ -7099,21 +7149,29 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
7099
7149
  `).trim();
7100
7150
  }
7101
7151
  function assembleFallback(summaries, extraction) {
7102
- const detModified = extraction.modifiedFiles.map((f) => f.path);
7103
- const detRead = extraction.readFiles;
7104
- const unresolved = extraction.errors.filter((error) => !error.resolved);
7105
- const inProgress = summaries.filter((item) => item.priority === "critical" || item.priority === "high").map((item) => "- [ ] " + item.summary.slice(0, TRUNC.PREVIEW));
7152
+ const safe = (value, max = TRUNC.PREVIEW_MID) => summaryEvidenceLine(value, max);
7153
+ const detModified = extraction.modifiedFiles.map((f) => safe(f.path)).filter(Boolean);
7154
+ const detRead = extraction.readFiles.map((file) => safe(file)).filter(Boolean);
7155
+ const unresolved = extraction.errors.filter((error) => !error.resolved).map((error) => safe(error.message, TRUNC.PREVIEW)).filter(Boolean);
7156
+ const constraints = extraction.constraints.map((item) => "- [" + item.category + "] " + safe(item.text)).filter((line) => !line.endsWith("] "));
7157
+ const decisions = extraction.decisions.map((item) => {
7158
+ const summary = safe(item.summary, TRUNC.DECISION_SUMMARY);
7159
+ const response = item.userResponse ? safe(item.userResponse, TRUNC.USER_RESPONSE) : "";
7160
+ return summary ? "- **" + summary + "**" + (response ? " \u2192 " + response : "") : "";
7161
+ }).filter(Boolean);
7162
+ const inProgress = summaries.filter((item) => item.priority === "critical" || item.priority === "high").map((item) => safe(item.summary, TRUNC.PREVIEW)).filter(Boolean).map((item) => "- [ ] " + item);
7106
7163
  if (!inProgress.length) {
7107
- inProgress.push(...extraction.lastUserMessages.slice(-3).map((message) => "- [ ] " + message.slice(0, TRUNC.PREVIEW)));
7164
+ inProgress.push(...extraction.lastUserMessages.slice(-3).map((message) => safe(message, TRUNC.PREVIEW)).filter(Boolean).map((message) => "- [ ] " + message));
7108
7165
  }
7109
7166
  inProgress.push(...detModified.map((file) => "- [ ] Continue work in " + file));
7110
- const next = extraction.lastUserMessages.at(-1)?.slice(0, TRUNC.PREVIEW) ?? extraction.timeline.at(-1)?.summary.slice(0, TRUNC.PREVIEW) ?? "Continue from the latest preserved context.";
7167
+ const next = safe(extraction.lastUserMessages.at(-1) ?? extraction.timeline.at(-1)?.summary ?? "", TRUNC.PREVIEW) || "Continue from the latest preserved context.";
7168
+ const goal = safe(extraction.mainGoal ?? "", TRUNC.DETAIL) || "Continue the current task.";
7111
7169
  return [
7112
7170
  "## Goal",
7113
- extraction.mainGoal ?? "Continue the current task.",
7171
+ goal,
7114
7172
  "",
7115
7173
  "## Constraints & Preferences",
7116
- ...extraction.constraints.length ? extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, TRUNC.PREVIEW_MID)) : ["- None recorded."],
7174
+ ...constraints.length ? constraints : ["- None recorded."],
7117
7175
  "",
7118
7176
  "## Progress",
7119
7177
  "### Done",
@@ -7121,25 +7179,28 @@ function assembleFallback(summaries, extraction) {
7121
7179
  "### In Progress",
7122
7180
  ...inProgress.length ? inProgress : ["- Continue current work."],
7123
7181
  "### Blocked",
7124
- ...unresolved.length ? unresolved.map((error) => "- " + error.message.slice(0, TRUNC.PREVIEW)) : ["- None recorded."],
7182
+ ...unresolved.length ? unresolved.map((error) => "- " + error) : ["- None recorded."],
7125
7183
  "",
7126
7184
  "## Key Decisions",
7127
- ...extraction.decisions.length ? extraction.decisions.map((d) => "- **" + d.summary.slice(0, TRUNC.TOPIC_LABEL) + "**" + (d.userResponse ? " \u2192 " + d.userResponse : "")) : ["- None recorded."],
7185
+ ...decisions.length ? decisions : ["- None recorded."],
7128
7186
  "",
7129
7187
  "## Files Modified",
7130
- ...detModified.length ? detModified.map((f) => "- " + f) : ["- None recorded."],
7188
+ ...detModified.length ? detModified.map((file) => "- " + file) : ["- None recorded."],
7131
7189
  "",
7132
7190
  "## Files Read",
7133
- ...detRead.length ? detRead.map((f) => "- " + f) : ["- None recorded."],
7191
+ ...detRead.length ? detRead.map((file) => "- " + file) : ["- None recorded."],
7134
7192
  "",
7135
7193
  "## Next Steps",
7136
7194
  "1. " + next,
7137
7195
  "",
7138
7196
  "## Critical Context",
7139
- ...unresolved.length ? unresolved.map((e) => "- Unresolved error: " + e.message.slice(0, TRUNC.TOPIC_LABEL)) : ["- None recorded."],
7197
+ ...unresolved.length ? unresolved.map((error) => "- Unresolved error: " + safe(error, TRUNC.TOPIC_LABEL)) : ["- None recorded."],
7140
7198
  "",
7141
7199
  "## Topics Covered",
7142
- ...summaries.length ? summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, TRUNC.PREVIEW_MID)) : extraction.topics.map((topic, index) => "- Topic " + (index + 1) + ": " + (topic.primaryFile ?? topic.type))
7200
+ ...summaries.length ? summaries.map((item) => {
7201
+ const topic = safe(item.topic, TRUNC.TOPIC_LABEL) || "Segment";
7202
+ return "- **" + topic + "** [" + item.priority + "]: " + safe(item.summary);
7203
+ }) : extraction.topics.map((topic, index) => "- Topic " + (index + 1) + ": " + safe(topic.primaryFile ?? topic.type))
7143
7204
  ].join(`
7144
7205
  `);
7145
7206
  }
@@ -7148,8 +7209,8 @@ function failedChunkSummary(ch) {
7148
7209
  topic: ch.topic,
7149
7210
  startIndex: ch.startIndex,
7150
7211
  endIndex: ch.endIndex,
7151
- summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
7152
- `).slice(0, TRUNC.DETAIL),
7212
+ summary: "[Failed] " + summaryEvidenceLine(ch.messages.map((m) => extractText(m.content)).join(`
7213
+ `), TRUNC.DETAIL),
7153
7214
  keyDecisions: [],
7154
7215
  filesModified: [],
7155
7216
  filesRead: [],
@@ -7530,6 +7591,21 @@ function verificationFailureMessage(result) {
7530
7591
  const findings = result.gaps.slice(0, 3).map((gap) => formatVerificationGap(gap).replace(/\s+/g, " ").slice(0, 160)).join("; ");
7531
7592
  return "Verification gate rejected summary (" + result.score + "/100, " + result.gaps.length + " unresolved gap(s))" + (findings ? ": " + findings : "");
7532
7593
  }
7594
+
7595
+ class VerificationGateError extends Error {
7596
+ score;
7597
+ initialScore;
7598
+ gapKinds;
7599
+ gapCount;
7600
+ constructor(result, initialScore) {
7601
+ super(verificationFailureMessage(result) ?? "Verification gate rejected summary");
7602
+ this.name = "VerificationGateError";
7603
+ this.score = result.score;
7604
+ this.initialScore = initialScore;
7605
+ this.gapKinds = Array.from(new Set(result.gaps.map((gap) => gap.kind)));
7606
+ this.gapCount = result.gaps.length;
7607
+ }
7608
+ }
7533
7609
  var NEGATION_MARKERS = new Set([
7534
7610
  "no",
7535
7611
  "not",
@@ -7600,14 +7676,15 @@ function semanticTokens(text) {
7600
7676
  return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
7601
7677
  }
7602
7678
  function evidenceFragments(text) {
7603
- return text.split(/(?:\r?\n|[.;])/).map((part) => part.replace(/^\s*[-*\d.)]+\s*/, "").trim()).filter(Boolean);
7679
+ const fragments = text.split(/\r?\n/).flatMap((line) => [line, ...line.split(/[.;]/)]).map((part) => part.replace(/^\s*[-*\d.)]+\s*/, "").trim()).filter(Boolean);
7680
+ return Array.from(new Set(fragments));
7604
7681
  }
7605
7682
  function hasNearbyMarker(tokens, anchor, markers) {
7606
7683
  return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
7607
7684
  }
7608
7685
  function semanticShape(source) {
7609
7686
  const sourceTokens = semanticTokens(source);
7610
- const concepts = Array.from(new Set(sourceTokens.filter((token) => !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
7687
+ const concepts = Array.from(new Set(sourceTokens.filter((token) => !/^\d+$/.test(token) && !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
7611
7688
  const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
7612
7689
  const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
7613
7690
  const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
@@ -7637,12 +7714,15 @@ function hasSemanticContradiction(source, target) {
7637
7714
  const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
7638
7715
  if (!anchor)
7639
7716
  return false;
7717
+ const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
7640
7718
  return evidenceFragments(target).some((fragment) => {
7641
7719
  const tokens = semanticTokens(fragment);
7642
7720
  if (!tokens.includes(anchor))
7643
7721
  return false;
7722
+ const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
7723
+ if (overlap < required)
7724
+ return false;
7644
7725
  if (negative && !hasNearbyMarker(tokens, anchor, NEGATION_MARKERS)) {
7645
- const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
7646
7726
  const validConditional = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
7647
7727
  return !validConditional;
7648
7728
  }
@@ -7656,6 +7736,28 @@ function isDeterministicallyPatchable(gap) {
7656
7736
  return gap.detail.startsWith("blocked-none:");
7657
7737
  return true;
7658
7738
  }
7739
+ function repairSummaryDeterministically(summary, result, extraction, continuity = null, maxRounds = 3) {
7740
+ const patched = [];
7741
+ const seen = new Set;
7742
+ for (let round = 0;round < maxRounds; round++) {
7743
+ const patchable = result.gaps.filter(isDeterministicallyPatchable);
7744
+ if (!patchable.length)
7745
+ break;
7746
+ const next = patchDeterministic(summary, patchable, extraction, continuity);
7747
+ if (next === summary)
7748
+ break;
7749
+ for (const gap of patchable) {
7750
+ const key = formatVerificationGap(gap);
7751
+ if (!seen.has(key)) {
7752
+ seen.add(key);
7753
+ patched.push(gap);
7754
+ }
7755
+ }
7756
+ summary = next;
7757
+ result = verifySummary(summary, extraction, continuity);
7758
+ }
7759
+ return { summary, result, patched };
7760
+ }
7659
7761
  function verifySummary(summary, extraction, continuity = null) {
7660
7762
  const parsed = parseSummary(summary);
7661
7763
  const gaps = [];
@@ -7806,15 +7908,15 @@ function verifySummary(summary, extraction, continuity = null) {
7806
7908
  }
7807
7909
  function patchDeterministic(summary, gaps, extraction, continuity = null) {
7808
7910
  let canonical = parseSummary(summary);
7809
- const verificationNotes = [];
7911
+ const safe = (value, max = TRUNC.MESSAGE) => summaryEvidenceLine(value, max);
7810
7912
  const unresolvedMessages = Array.from(new Set([
7811
7913
  ...extraction.errors.filter((error) => !error.resolved).map((error) => error.message),
7812
7914
  ...(continuity?.unresolvedErrors ?? []).map((error) => error.message)
7813
7915
  ]));
7814
7916
  const unresolvedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved");
7815
7917
  const blockedItems = [
7816
- ...unresolvedMessages.map((message) => "- " + message.slice(0, TRUNC.MESSAGE)),
7817
- ...unresolvedLoops.map((loop) => "- " + loop.summary.slice(0, TRUNC.MESSAGE))
7918
+ ...unresolvedMessages.map((message) => safe(message)).filter(Boolean).map((message) => "- " + message),
7919
+ ...unresolvedLoops.map((loop) => safe(loop.summary)).filter(Boolean).map((summary2) => "- " + summary2)
7818
7920
  ];
7819
7921
  const patchBlockedNone = () => {
7820
7922
  const progress = findSection(canonical, "progress");
@@ -7828,7 +7930,7 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7828
7930
  switch (gap.kind) {
7829
7931
  case "missing-section": {
7830
7932
  if (gap.section === "goal") {
7831
- canonical = upsertSection(canonical, "goal", extraction.mainGoal ?? "Continue the current coding task.");
7933
+ canonical = upsertSection(canonical, "goal", safe(extraction.mainGoal ?? "", TRUNC.DETAIL) || "Continue the current coding task.");
7832
7934
  } else if (gap.section === "progress") {
7833
7935
  canonical = upsertSection(canonical, "progress", `### Done
7834
7936
  - No explicit completion recorded.
@@ -7838,36 +7940,36 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7838
7940
  ` + (blockedItems.join(`
7839
7941
  `) || "- None recorded."));
7840
7942
  } else if (gap.section === "critical-context") {
7841
- const critical = unresolvedMessages.map((message) => "- Unresolved error: " + message.slice(0, TRUNC.MESSAGE));
7943
+ const critical = unresolvedMessages.map((message) => safe(message)).filter(Boolean).map((message) => "- Unresolved error: " + message);
7842
7944
  canonical = upsertSection(canonical, "critical-context", critical.join(`
7843
7945
  `) || "- None recorded.");
7844
7946
  }
7845
7947
  break;
7846
7948
  }
7847
7949
  case "missing-file":
7848
- canonical = appendToSection(canonical, "files-modified", "- " + gap.path);
7950
+ canonical = appendToSection(canonical, "files-modified", "- " + safe(gap.path));
7849
7951
  break;
7850
7952
  case "missing-error": {
7851
7953
  const existing = findSection(canonical, "critical-context")?.body.toLowerCase() ?? "";
7852
- const message = gap.message.slice(0, TRUNC.MESSAGE);
7954
+ const message = safe(gap.message);
7853
7955
  if (!existing.includes(message.toLowerCase())) {
7854
7956
  canonical = appendToSection(canonical, "critical-context", "- Unresolved error: " + message);
7855
7957
  }
7856
7958
  break;
7857
7959
  }
7858
7960
  case "missing-constraint":
7859
- canonical = appendToSection(canonical, "constraints", "- " + gap.text.slice(0, TRUNC.CONSTRAINT_TEXT));
7961
+ canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
7860
7962
  break;
7861
7963
  case "missing-decision":
7862
- canonical = appendToSection(canonical, "decisions", "- **" + gap.summary.slice(0, TRUNC.DECISION_DETAIL) + "**");
7964
+ canonical = appendToSection(canonical, "decisions", "- **" + safe(gap.summary, TRUNC.DECISION_SUMMARY) + "**");
7863
7965
  break;
7864
7966
  case "missing-goal":
7865
- canonical = upsertSection(canonical, "goal", gap.goal);
7967
+ canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
7866
7968
  break;
7867
7969
  case "missing-open-loops": {
7868
- const current = extraction.errors.filter((error) => !error.resolved).map((error) => "- [high] Resolve " + error.message.slice(0, TRUNC.SNIPPET));
7869
- const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error) => "- [high] Resolve " + error.message.slice(0, TRUNC.SNIPPET));
7870
- const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => "- [" + loop.priority + "] " + loop.summary.slice(0, TRUNC.SNIPPET));
7970
+ const current = extraction.errors.filter((error) => !error.resolved).map((error) => safe(error.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
7971
+ const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error) => safe(error.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
7972
+ 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);
7871
7973
  const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
7872
7974
  `);
7873
7975
  canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
@@ -7875,7 +7977,6 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7875
7977
  }
7876
7978
  case "fabricated-file": {
7877
7979
  const normalizedRef = gap.ref.replace(/\\/g, "/").toLowerCase();
7878
- let removed = false;
7879
7980
  canonical = {
7880
7981
  sections: canonical.sections.map((section) => ({
7881
7982
  ...section,
@@ -7884,42 +7985,32 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7884
7985
  if (!/^\s*[-*]\s+/.test(line))
7885
7986
  return true;
7886
7987
  const matches = extractFileRefs(line).some((ref) => ref.replace(/\\/g, "/").toLowerCase() === normalizedRef);
7887
- if (matches)
7888
- removed = true;
7889
7988
  return !matches;
7890
7989
  }).join(`
7891
7990
  `).trim()
7892
7991
  }))
7893
7992
  };
7894
- if (!removed)
7895
- verificationNotes.push(formatVerificationGap(gap));
7896
7993
  break;
7897
7994
  }
7898
7995
  case "inconsistency":
7899
7996
  if (gap.detail.startsWith("blocked-none:"))
7900
7997
  patchBlockedNone();
7901
- else
7902
- verificationNotes.push(formatVerificationGap(gap));
7903
7998
  break;
7904
7999
  }
7905
8000
  }
7906
- if (verificationNotes.length > 0) {
7907
- canonical = upsertSection(canonical, "verification-note", verificationNotes.map((note) => "- " + note).join(`
7908
- `));
7909
- }
7910
8001
  return renderSummary(canonical, { canonicalHeadings: true });
7911
8002
  }
7912
8003
  async function patchSummary(summary, gaps, model, auth, signal, services) {
7913
- const patchPrompt = `The summary below is missing some critical information. Add the missing items WITHOUT restructuring the summary.
8004
+ const patchPrompt = `Correct every verification finding below WITHOUT restructuring the summary. Add missing evidence, remove fabricated references, and rewrite contradictory claims so they preserve the source constraint/decision polarity. Do not add a Verification Note.
7914
8005
 
7915
- Missing items:
8006
+ Findings:
7916
8007
  ` + gaps.map((gap, index) => index + 1 + ". " + formatVerificationGap(gap)).join(`
7917
8008
  `) + `
7918
8009
 
7919
8010
  Current summary:
7920
8011
  ` + summary + `
7921
8012
 
7922
- Return the COMPLETE updated summary with missing items integrated. Keep the same format.`;
8013
+ Return the COMPLETE corrected summary in the same format.`;
7923
8014
  try {
7924
8015
  const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
7925
8016
  const response = await trackedComplete("patch", model, {
@@ -7939,10 +8030,6 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
7939
8030
  init_overlays();
7940
8031
  init_logger();
7941
8032
  init_mode_policy();
7942
- function informationTokenCount(summary) {
7943
- const ignored = new Set(["none", "recorded", "continue", "current", "work", "explicit", "completion"]);
7944
- return new Set((summary.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) ?? []).filter((token) => !ignored.has(token))).size;
7945
- }
7946
8033
  async function verifyAndPatch(rc) {
7947
8034
  const extraction = rc.extraction;
7948
8035
  let summary = rc.finalSummary;
@@ -7959,17 +8046,20 @@ async function verifyAndPatch(rc) {
7959
8046
  }
7960
8047
  let verification = verifySummary(summary, extraction, rc.previousState);
7961
8048
  const initialScore = verification.score;
7962
- const deterministicPatched = verification.gaps.filter(isDeterministicallyPatchable);
8049
+ const deterministicPatched = [];
7963
8050
  let llmPatched = false;
7964
8051
  let qualityFloorUsed = false;
7965
8052
  rc.vlog("Verification score=" + verification.score + " ok=" + verification.ok + " gaps=" + verification.gaps.length);
7966
- if (deterministicPatched.length > 0) {
7967
- rc.notify("Phase 4 Verify: " + deterministicPatched.length + " deterministic gap(s), score=" + verification.score + ", applying repair", "warning");
7968
- summary = patchDeterministic(summary, verification.gaps, extraction, rc.previousState);
7969
- verification = verifySummary(summary, extraction, rc.previousState);
8053
+ const initialPatchable = verification.gaps.filter(isDeterministicallyPatchable);
8054
+ if (initialPatchable.length > 0) {
8055
+ rc.notify("Phase 4 Verify: " + initialPatchable.length + " deterministic gap(s), score=" + verification.score + ", applying repair", "warning");
8056
+ const repaired = repairSummaryDeterministically(summary, verification, extraction, rc.previousState);
8057
+ summary = repaired.summary;
8058
+ verification = repaired.result;
8059
+ deterministicPatched.push(...repaired.patched);
7970
8060
  }
7971
8061
  const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
7972
- if (MODE_POLICIES[mode].allowLlmPatch && !verification.ok && verification.score < 75) {
8062
+ if (MODE_POLICIES[mode].allowLlmPatch && !verification.ok) {
7973
8063
  rc.notify("Phase 4 Verify: deterministic repair insufficient (score=" + verification.score + "), requesting LLM patch", "warning");
7974
8064
  const beforePatch = summary;
7975
8065
  try {
@@ -7981,34 +8071,30 @@ async function verifyAndPatch(rc) {
7981
8071
  if (summary !== beforePatch) {
7982
8072
  llmPatched = true;
7983
8073
  verification = verifySummary(summary, extraction, rc.previousState);
8074
+ const repaired = repairSummaryDeterministically(summary, verification, extraction, rc.previousState);
8075
+ summary = repaired.summary;
8076
+ verification = repaired.result;
8077
+ deterministicPatched.push(...repaired.patched);
7984
8078
  }
7985
8079
  }
7986
8080
  if (!verification.ok) {
7987
8081
  let deterministic = assembleFallback(rc.summaries, extraction);
7988
8082
  let deterministicVerification = verifySummary(deterministic, extraction, rc.previousState);
7989
- const patchable = deterministicVerification.gaps.filter(isDeterministicallyPatchable);
7990
- if (patchable.length > 0) {
7991
- deterministic = patchDeterministic(deterministic, deterministicVerification.gaps, extraction, rc.previousState);
7992
- deterministicVerification = verifySummary(deterministic, extraction, rc.previousState);
7993
- }
7994
- const gain = deterministicVerification.score - verification.score;
7995
- const currentInformation = Math.max(1, informationTokenCount(summary));
7996
- const fallbackCoverage = informationTokenCount(deterministic) / currentInformation;
7997
- const semanticSafetyFailure = verification.gaps.some((gap) => gap.kind === "inconsistency" && gap.detail.startsWith("semantic-contradiction:"));
7998
- const catastrophic = verification.score < 50;
7999
- const materiallyBetter = gain >= 15 && fallbackCoverage >= 0.65;
8000
- if (deterministicVerification.ok && (semanticSafetyFailure || catastrophic || materiallyBetter)) {
8083
+ const repaired = repairSummaryDeterministically(deterministic, deterministicVerification, extraction, rc.previousState);
8084
+ deterministic = repaired.summary;
8085
+ deterministicVerification = repaired.result;
8086
+ if (deterministicVerification.ok) {
8001
8087
  summary = deterministic;
8002
8088
  verification = deterministicVerification;
8003
- deterministicPatched.push(...patchable);
8089
+ deterministicPatched.push(...repaired.patched);
8004
8090
  qualityFloorUsed = true;
8005
- rc.notify("Quality floor replaced unsafe or materially lower-coverage model output", "warning");
8091
+ rc.notify("Quality floor replaced unsafe or unverifiable model output", "warning");
8006
8092
  }
8007
8093
  }
8008
8094
  const failure = verificationFailureMessage(verification);
8009
8095
  if (failure) {
8010
8096
  rc.notify(failure + " \u2014 current conversation left unchanged", "error");
8011
- throw new Error(failure);
8097
+ throw new VerificationGateError(verification, initialScore);
8012
8098
  }
8013
8099
  const out = rc;
8014
8100
  out.finalSummary = summary;
@@ -8083,24 +8169,23 @@ function buildState(rc) {
8083
8169
  summary = rc.services.scrubber.scrubText(summary).value;
8084
8170
  compactionState = rc.services.scrubber.scrubValue(compactionState).value;
8085
8171
  let postVerification = verifySummary(summary, extraction, compactionState);
8086
- const postPatchable = postVerification.gaps.filter(isDeterministicallyPatchable);
8087
- if (postPatchable.length > 0) {
8088
- summary = patchDeterministic(summary, postVerification.gaps, extraction, compactionState);
8089
- postVerification = verifySummary(summary, extraction, compactionState);
8090
- }
8172
+ const postInitialScore = postVerification.score;
8173
+ const postRepair = repairSummaryDeterministically(summary, postVerification, extraction, compactionState);
8174
+ summary = postRepair.summary;
8175
+ postVerification = postRepair.result;
8091
8176
  rc.verified = postVerification.ok;
8092
8177
  rc.verificationScore = postVerification.score;
8093
8178
  rc.verificationGaps = postVerification.gaps.map(formatVerificationGap);
8094
8179
  rc.verificationProvenance = {
8095
8180
  ...rc.verificationProvenance,
8096
- deterministicPatched: [...rc.verificationProvenance.deterministicPatched, ...postPatchable],
8181
+ deterministicPatched: [...rc.verificationProvenance.deterministicPatched, ...postRepair.patched],
8097
8182
  finalScore: postVerification.score,
8098
8183
  remainingGaps: postVerification.gaps
8099
8184
  };
8100
8185
  const failure = verificationFailureMessage(postVerification);
8101
8186
  if (failure) {
8102
8187
  rc.notify(failure + " after continuity injection \u2014 current conversation left unchanged", "error");
8103
- throw new Error(failure);
8188
+ throw new VerificationGateError(postVerification, postInitialScore);
8104
8189
  }
8105
8190
  const detModified = extraction.modifiedFiles.map((f) => f.path);
8106
8191
  const detRead = extraction.readFiles;
@@ -8164,11 +8249,37 @@ var MAX_MANUAL_NODES = 500;
8164
8249
  var MAX_SESSION_NODES = 256;
8165
8250
  var MAX_QUERY_CANDIDATES = 80;
8166
8251
  var NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
8252
+ function nodeSqliteAdapter(db) {
8253
+ return {
8254
+ exec: (sql) => db.exec(sql),
8255
+ query: (sql) => db.prepare(sql),
8256
+ transaction: (fn) => (...args) => {
8257
+ db.exec("BEGIN IMMEDIATE");
8258
+ try {
8259
+ const result = fn(...args);
8260
+ db.exec("COMMIT");
8261
+ return result;
8262
+ } catch (error) {
8263
+ try {
8264
+ db.exec("ROLLBACK");
8265
+ } catch {}
8266
+ throw error;
8267
+ }
8268
+ },
8269
+ close: () => db.close()
8270
+ };
8271
+ }
8167
8272
  function openDatabase() {
8168
8273
  const fp = contextGraphFile();
8169
8274
  fs7.mkdirSync(path10.dirname(fp), { recursive: true });
8170
- const { Database } = require2("bun:sqlite");
8171
- const db = new Database(fp);
8275
+ let db;
8276
+ if ("bun" in process.versions) {
8277
+ const { Database } = require2("bun:sqlite");
8278
+ db = new Database(fp);
8279
+ } else {
8280
+ const { DatabaseSync } = require2("node:sqlite");
8281
+ db = nodeSqliteAdapter(new DatabaseSync(fp));
8282
+ }
8172
8283
  try {
8173
8284
  fs7.chmodSync(fp, 384);
8174
8285
  } catch {}
@@ -8835,6 +8946,22 @@ function recordSuccessMetrics(rc, status) {
8835
8946
  function recordFailureMetrics(rc, err, fields) {
8836
8947
  const releaseChannel = rc.config?.telemetryChannel ?? loadConfig().telemetryChannel;
8837
8948
  const failureKind = classifyTelemetryFailure(err, rc.cancellation.timedOut);
8949
+ const gate = err && typeof err === "object" ? err : null;
8950
+ const knownGapKinds = new Set([
8951
+ "missing-section",
8952
+ "missing-file",
8953
+ "missing-error",
8954
+ "missing-constraint",
8955
+ "missing-decision",
8956
+ "missing-goal",
8957
+ "fabricated-file",
8958
+ "inconsistency",
8959
+ "missing-open-loops"
8960
+ ]);
8961
+ const gapKinds = Array.isArray(gate?.gapKinds) ? gate.gapKinds.filter((kind) => typeof kind === "string" && knownGapKinds.has(kind)) : undefined;
8962
+ const verificationScore = typeof gate?.score === "number" && Number.isFinite(gate.score) ? gate.score : undefined;
8963
+ const initialVerificationScore = typeof gate?.initialScore === "number" && Number.isFinite(gate.initialScore) ? gate.initialScore : undefined;
8964
+ const verificationGaps = typeof gate?.gapCount === "number" && Number.isInteger(gate.gapCount) && gate.gapCount >= 0 ? gate.gapCount : undefined;
8838
8965
  appendMetricsLog(fields.sessionId ?? "unknown", {
8839
8966
  runId: rc.runId,
8840
8967
  metricsSchemaVersion: 2,
@@ -8854,6 +8981,11 @@ function recordFailureMetrics(rc, err, fields) {
8854
8981
  runType: runType(rc),
8855
8982
  status: rc.cancellation.timedOut ? "timeout" : "error",
8856
8983
  fallbackReason: "failure:" + failureKind,
8984
+ verificationScore,
8985
+ initialVerificationScore,
8986
+ verificationGaps,
8987
+ remainingVerificationGaps: verificationGaps,
8988
+ verificationGapKinds: gapKinds,
8857
8989
  phaseTimings: rc.phaseTimings,
8858
8990
  durationMs: Date.now() - rc.pipelineStart
8859
8991
  }, rc.services);
@@ -8989,7 +9121,8 @@ function makeBase(opts) {
8989
9121
  dryRun: !!opts.dryRun,
8990
9122
  autoTriggered: !!opts.autoTriggered,
8991
9123
  skipCompact: !!opts.skipCompact,
8992
- force: !!opts.force
9124
+ force: !!opts.force,
9125
+ overflowRecovery: !!opts.overflowRecovery
8993
9126
  },
8994
9127
  userNote: opts.userNote,
8995
9128
  focus: opts.focus,
@@ -9015,8 +9148,11 @@ async function runSmartCompact(opts) {
9015
9148
  return;
9016
9149
  }
9017
9150
  const runSessionId = resolveSessionId(opts.ctx);
9018
- if (!acquireRunLock(opts.isRunning, runSessionId))
9151
+ if (!acquireRunLock(opts.isRunning, runSessionId)) {
9152
+ if (!opts.autoTriggered)
9153
+ opts.ctx.ui.notify("Smart compact is already running for this session.", "warning");
9019
9154
  return;
9155
+ }
9020
9156
  const base = makeBase(opts);
9021
9157
  const abortFromHost = () => {
9022
9158
  base.cancellation.timedOut = true;
@@ -9786,8 +9922,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9786
9922
  const totalTokens = usage?.tokens ?? 0;
9787
9923
  const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
9788
9924
  if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
9789
- ctx.ui.notify("Context OK or unknown", "info");
9790
- return;
9925
+ ctx.ui.notify("Context usage is low or unknown (" + totalTokens.toLocaleString() + "t). Manual compaction may save little and can lose nuance; continuing because you requested it.", "warning");
9791
9926
  }
9792
9927
  const cur = ctx.model;
9793
9928
  const avail = ctx.modelRegistry.getAvailable();
@@ -9856,7 +9991,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9856
9991
  if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
9857
9992
  return;
9858
9993
  const pct = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
9859
- if (pct < config.minContextPercent)
9994
+ if (event.reason !== "overflow" && pct < config.minContextPercent)
9860
9995
  return;
9861
9996
  const cur = ctx.model;
9862
9997
  if (!cur)
@@ -9885,6 +10020,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9885
10020
  isRunning,
9886
10021
  onNativeApplyError,
9887
10022
  autoTriggered: true,
10023
+ overflowRecovery: event.reason === "overflow",
9888
10024
  timeoutMs: effectiveTimeoutMs,
9889
10025
  cancellationOut
9890
10026
  });