pi-smart-compact 8.0.1 → 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.1", 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(`
@@ -3824,9 +3839,17 @@ import fs5 from "fs";
3824
3839
  function getStatePath(projectId, state) {
3825
3840
  return state?.scope?.sessionId ? scopedCompactionStateFile(projectId, state.scope.sessionId) : compactionStateFile(projectId);
3826
3841
  }
3842
+ function isLegacySearchOutput(text) {
3843
+ const firstLine = text.trim().split(/\r?\n/, 1)[0] ?? "";
3844
+ return /^[^\s:][^:]*:\d+(?::\d+)?:/.test(firstLine);
3845
+ }
3827
3846
  function sanitizeCompactionStateEvidence(state) {
3828
3847
  const constraints = state.constraints.filter((item) => !isDiagnosticConstraintText(item.text));
3829
- return constraints.length === state.constraints.length ? state : { ...state, constraints };
3848
+ const unresolvedErrors = state.unresolvedErrors.filter((item) => !isLegacySearchOutput(item.message));
3849
+ const openLoops = state.openLoops.filter((item) => !isLegacySearchOutput(item.summary));
3850
+ if (constraints.length === state.constraints.length && unresolvedErrors.length === state.unresolvedErrors.length && openLoops.length === state.openLoops.length)
3851
+ return state;
3852
+ return { ...state, constraints, unresolvedErrors, openLoops };
3830
3853
  }
3831
3854
  function freshState(fp, data) {
3832
3855
  if (!data)
@@ -3995,7 +4018,7 @@ function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, exi
3995
4018
  const haystack = normalizeFactKey(existing);
3996
4019
  const lines = ["## Continuity Ledger"];
3997
4020
  const add = (label, value) => {
3998
- const text = value.trim();
4021
+ const text = summaryEvidenceLine(value, TRUNC.MESSAGE);
3999
4022
  if (!text || haystack.includes(normalizeFactKey(text)))
4000
4023
  return;
4001
4024
  const line = "- " + label + ": " + text;
@@ -4023,8 +4046,9 @@ function injectOpenLoopsSection(summary, openLoops) {
4023
4046
  return summary;
4024
4047
  const body = openLoops.map((l) => {
4025
4048
  const prio = l.priority === "critical" || l.priority === "high" ? "[" + l.priority + "] " : "";
4026
- const files = l.files.length ? " \u2014 " + l.files.join(", ") : "";
4027
- 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;
4028
4052
  }).join(`
4029
4053
  `);
4030
4054
  const parsed = parseSummary(summary);
@@ -4073,32 +4097,33 @@ function hasDeltaChanges(delta) {
4073
4097
  }
4074
4098
  function formatDeltaSection(delta) {
4075
4099
  const lines = ["## Changes Since Last Compaction", ""];
4100
+ const safe = (value, max) => summaryEvidenceLine(value, max);
4076
4101
  if (delta.goalChanged) {
4077
- 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");
4078
4103
  }
4079
4104
  if (delta.resolvedLoops.length) {
4080
- 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(", "));
4081
4106
  }
4082
4107
  if (delta.persistentLoops.length) {
4083
- 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("; "));
4084
4109
  }
4085
4110
  if (delta.newLoops.length) {
4086
- 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("; "));
4087
4112
  }
4088
4113
  if (delta.newDecisions.length) {
4089
- 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("; "));
4090
4115
  }
4091
4116
  if (delta.removedDecisions.length) {
4092
- 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("; "));
4093
4118
  }
4094
4119
  if (delta.resolvedErrors.length) {
4095
- 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("; "));
4096
4121
  }
4097
4122
  if (delta.newErrors.length) {
4098
- 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("; "));
4099
4124
  }
4100
4125
  if (delta.newModifiedFiles.length) {
4101
- lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
4126
+ lines.push("- **New files touched**: " + delta.newModifiedFiles.map((file) => safe(file, TRUNC.MESSAGE)).join(", "));
4102
4127
  }
4103
4128
  lines.push("");
4104
4129
  return lines.join(`
@@ -4120,7 +4145,7 @@ function ensurePinnedPaths(summary, pinned) {
4120
4145
  if (!pinned.length)
4121
4146
  return summary;
4122
4147
  const lower = summary.toLowerCase();
4123
- 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()));
4124
4149
  if (!missing.length)
4125
4150
  return summary;
4126
4151
  const parsed = parseSummary(summary);
@@ -5717,33 +5742,42 @@ async function prepareRun(rc) {
5717
5742
  // src/app/steps/window.ts
5718
5743
  init_mode_policy();
5719
5744
  init_helpers();
5745
+ init_constants();
5720
5746
  function resolveCompactionWindow(rc) {
5721
5747
  const usage = rc.ctx.getContextUsage();
5722
5748
  const totalTokens = usage?.tokens ?? 0;
5723
5749
  const manager = rc.ctx.sessionManager;
5724
5750
  const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
5725
5751
  const msgs = branch.filter((e) => e.type === "message" && e.message != null);
5726
- 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");
5727
5755
  return null;
5756
+ }
5728
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;
5729
5758
  const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
5730
5759
  const targetPercent = MODE_POLICIES[mode].targetContextPercent;
5731
5760
  const messageTokens = msgs.map((entry) => rc.estimator.message(entry.message));
5732
5761
  const allMessageTokens = messageTokens.reduce((sum, tokens) => sum + tokens, 0);
5733
- 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);
5734
5765
  const targetRetainedTokens = rc.ctx.model ? Math.max(0, rc.ctx.model.contextWindow * targetPercent / 100 - fixedContextTokens - rc.profileCfg.summaryBudgetTokens) : adaptiveKeepTokens;
5735
- 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;
5736
5769
  let accTokens = 0;
5737
5770
  let keepFrom = msgs.length - 1;
5738
5771
  for (let i = msgs.length - 1;i >= 0; i--) {
5739
5772
  const next = messageTokens[i];
5740
- if (accTokens >= adaptiveKeepTokens && accTokens + next > retentionBudget) {
5773
+ if (accTokens >= rawAdaptiveKeepTokens && accTokens + next > rawRetentionBudget) {
5741
5774
  keepFrom = i + 1;
5742
5775
  break;
5743
5776
  }
5744
5777
  accTokens += next;
5745
5778
  keepFrom = i;
5746
5779
  }
5780
+ const plannedKeepFrom = keepFrom;
5747
5781
  const recentUsers = msgs.map((entry, index) => ({ index, role: entry.message?.role })).filter((entry) => entry.role === "user");
5748
5782
  if (recentUsers.length) {
5749
5783
  const protectedUser = recentUsers[recentUsers.length >= 2 ? recentUsers.length - 2 : recentUsers.length - 1];
@@ -5751,19 +5785,42 @@ function resolveCompactionWindow(rc) {
5751
5785
  }
5752
5786
  keepFrom = smartKeepBoundary(msgs, keepFrom, branch);
5753
5787
  keepFrom = guardToolCallBoundary(msgs, keepFrom);
5754
- const rawCompactTokens = messageTokens.slice(0, keepFrom).reduce((sum, tokens) => sum + tokens, 0);
5755
- const rawRetainedTokens = messageTokens.slice(keepFrom).reduce((sum, tokens) => sum + tokens, 0);
5756
- const messageScale = totalTokens > 0 && allMessageTokens > totalTokens ? totalTokens / allMessageTokens : 1;
5757
- const compactTokens = Math.round(rawCompactTokens * messageScale);
5758
- 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;
5759
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");
5760
5805
  return null;
5761
5806
  }
5762
5807
  const toCompact = msgs.slice(0, keepFrom);
5763
- 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
+ }
5764
5812
  return null;
5813
+ }
5765
5814
  const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
5766
- 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) {
5767
5824
  const projectedTokens = fixedContextTokens + accTokens + rc.profileCfg.summaryBudgetTokens;
5768
5825
  const targetTokens = rc.ctx.model.contextWindow * targetPercent / 100;
5769
5826
  if (projectedTokens > targetTokens) {
@@ -6024,7 +6081,7 @@ init_constants();
6024
6081
  init_helpers();
6025
6082
  function selectTier(rc) {
6026
6083
  const toolPercent = computeToolCharPercentage(rc.branch);
6027
- 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);
6028
6085
  if (tier === "none") {
6029
6086
  if (!rc.flags.autoTriggered) {
6030
6087
  rc.ctx.ui.notify("Context OK (" + Math.round(rc.contextPercent) + "%). pi-toolkit manages context well.", "info");
@@ -6845,6 +6902,7 @@ function getCachedBatchClone(value) {
6845
6902
  }
6846
6903
 
6847
6904
  // src/phases/synthesize.ts
6905
+ init_summary_parse();
6848
6906
  function boundedToolArgs(value, depth = 0) {
6849
6907
  if (typeof value === "string")
6850
6908
  return value.length > TRUNC.DETAIL ? value.slice(0, TRUNC.DETAIL) + "\u2026" : value;
@@ -7091,21 +7149,29 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
7091
7149
  `).trim();
7092
7150
  }
7093
7151
  function assembleFallback(summaries, extraction) {
7094
- const detModified = extraction.modifiedFiles.map((f) => f.path);
7095
- const detRead = extraction.readFiles;
7096
- const unresolved = extraction.errors.filter((error) => !error.resolved);
7097
- 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);
7098
7163
  if (!inProgress.length) {
7099
- 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));
7100
7165
  }
7101
7166
  inProgress.push(...detModified.map((file) => "- [ ] Continue work in " + file));
7102
- 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.";
7103
7169
  return [
7104
7170
  "## Goal",
7105
- extraction.mainGoal ?? "Continue the current task.",
7171
+ goal,
7106
7172
  "",
7107
7173
  "## Constraints & Preferences",
7108
- ...extraction.constraints.length ? extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, TRUNC.PREVIEW_MID)) : ["- None recorded."],
7174
+ ...constraints.length ? constraints : ["- None recorded."],
7109
7175
  "",
7110
7176
  "## Progress",
7111
7177
  "### Done",
@@ -7113,25 +7179,28 @@ function assembleFallback(summaries, extraction) {
7113
7179
  "### In Progress",
7114
7180
  ...inProgress.length ? inProgress : ["- Continue current work."],
7115
7181
  "### Blocked",
7116
- ...unresolved.length ? unresolved.map((error) => "- " + error.message.slice(0, TRUNC.PREVIEW)) : ["- None recorded."],
7182
+ ...unresolved.length ? unresolved.map((error) => "- " + error) : ["- None recorded."],
7117
7183
  "",
7118
7184
  "## Key Decisions",
7119
- ...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."],
7120
7186
  "",
7121
7187
  "## Files Modified",
7122
- ...detModified.length ? detModified.map((f) => "- " + f) : ["- None recorded."],
7188
+ ...detModified.length ? detModified.map((file) => "- " + file) : ["- None recorded."],
7123
7189
  "",
7124
7190
  "## Files Read",
7125
- ...detRead.length ? detRead.map((f) => "- " + f) : ["- None recorded."],
7191
+ ...detRead.length ? detRead.map((file) => "- " + file) : ["- None recorded."],
7126
7192
  "",
7127
7193
  "## Next Steps",
7128
7194
  "1. " + next,
7129
7195
  "",
7130
7196
  "## Critical Context",
7131
- ...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."],
7132
7198
  "",
7133
7199
  "## Topics Covered",
7134
- ...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))
7135
7204
  ].join(`
7136
7205
  `);
7137
7206
  }
@@ -7140,8 +7209,8 @@ function failedChunkSummary(ch) {
7140
7209
  topic: ch.topic,
7141
7210
  startIndex: ch.startIndex,
7142
7211
  endIndex: ch.endIndex,
7143
- summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
7144
- `).slice(0, TRUNC.DETAIL),
7212
+ summary: "[Failed] " + summaryEvidenceLine(ch.messages.map((m) => extractText(m.content)).join(`
7213
+ `), TRUNC.DETAIL),
7145
7214
  keyDecisions: [],
7146
7215
  filesModified: [],
7147
7216
  filesRead: [],
@@ -7522,6 +7591,21 @@ function verificationFailureMessage(result) {
7522
7591
  const findings = result.gaps.slice(0, 3).map((gap) => formatVerificationGap(gap).replace(/\s+/g, " ").slice(0, 160)).join("; ");
7523
7592
  return "Verification gate rejected summary (" + result.score + "/100, " + result.gaps.length + " unresolved gap(s))" + (findings ? ": " + findings : "");
7524
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
+ }
7525
7609
  var NEGATION_MARKERS = new Set([
7526
7610
  "no",
7527
7611
  "not",
@@ -7592,14 +7676,15 @@ function semanticTokens(text) {
7592
7676
  return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
7593
7677
  }
7594
7678
  function evidenceFragments(text) {
7595
- 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));
7596
7681
  }
7597
7682
  function hasNearbyMarker(tokens, anchor, markers) {
7598
7683
  return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
7599
7684
  }
7600
7685
  function semanticShape(source) {
7601
7686
  const sourceTokens = semanticTokens(source);
7602
- 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))));
7603
7688
  const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
7604
7689
  const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
7605
7690
  const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
@@ -7629,12 +7714,15 @@ function hasSemanticContradiction(source, target) {
7629
7714
  const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
7630
7715
  if (!anchor)
7631
7716
  return false;
7717
+ const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
7632
7718
  return evidenceFragments(target).some((fragment) => {
7633
7719
  const tokens = semanticTokens(fragment);
7634
7720
  if (!tokens.includes(anchor))
7635
7721
  return false;
7722
+ const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
7723
+ if (overlap < required)
7724
+ return false;
7636
7725
  if (negative && !hasNearbyMarker(tokens, anchor, NEGATION_MARKERS)) {
7637
- const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
7638
7726
  const validConditional = sourceTokens.includes("without") && tokens.some((token) => CONDITION_MARKERS.has(token)) && overlap >= Math.min(2, concepts.length);
7639
7727
  return !validConditional;
7640
7728
  }
@@ -7648,6 +7736,28 @@ function isDeterministicallyPatchable(gap) {
7648
7736
  return gap.detail.startsWith("blocked-none:");
7649
7737
  return true;
7650
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
+ }
7651
7761
  function verifySummary(summary, extraction, continuity = null) {
7652
7762
  const parsed = parseSummary(summary);
7653
7763
  const gaps = [];
@@ -7767,7 +7877,10 @@ function verifySummary(summary, extraction, continuity = null) {
7767
7877
  const basename = file.path.split("/").pop() ?? "";
7768
7878
  if (!doneSection.toLowerCase().includes(basename.toLowerCase()))
7769
7879
  continue;
7770
- const unresolved = unresolvedEvidence.find((error) => error.message.toLowerCase().includes(basename.toLowerCase()));
7880
+ const unresolved = unresolvedEvidence.find((error) => {
7881
+ const firstLine = error.message.split(/\r?\n/, 1)[0] ?? "";
7882
+ return extractFileRefs(firstLine).some((ref) => isKnownPathReference(ref, [file.path]));
7883
+ });
7771
7884
  if (unresolved) {
7772
7885
  gaps.push({ kind: "inconsistency", detail: basename + " marked Done but has unresolved error" });
7773
7886
  score -= 5;
@@ -7795,15 +7908,15 @@ function verifySummary(summary, extraction, continuity = null) {
7795
7908
  }
7796
7909
  function patchDeterministic(summary, gaps, extraction, continuity = null) {
7797
7910
  let canonical = parseSummary(summary);
7798
- const verificationNotes = [];
7911
+ const safe = (value, max = TRUNC.MESSAGE) => summaryEvidenceLine(value, max);
7799
7912
  const unresolvedMessages = Array.from(new Set([
7800
7913
  ...extraction.errors.filter((error) => !error.resolved).map((error) => error.message),
7801
7914
  ...(continuity?.unresolvedErrors ?? []).map((error) => error.message)
7802
7915
  ]));
7803
7916
  const unresolvedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved");
7804
7917
  const blockedItems = [
7805
- ...unresolvedMessages.map((message) => "- " + message.slice(0, TRUNC.MESSAGE)),
7806
- ...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)
7807
7920
  ];
7808
7921
  const patchBlockedNone = () => {
7809
7922
  const progress = findSection(canonical, "progress");
@@ -7817,7 +7930,7 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7817
7930
  switch (gap.kind) {
7818
7931
  case "missing-section": {
7819
7932
  if (gap.section === "goal") {
7820
- 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.");
7821
7934
  } else if (gap.section === "progress") {
7822
7935
  canonical = upsertSection(canonical, "progress", `### Done
7823
7936
  - No explicit completion recorded.
@@ -7827,36 +7940,36 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7827
7940
  ` + (blockedItems.join(`
7828
7941
  `) || "- None recorded."));
7829
7942
  } else if (gap.section === "critical-context") {
7830
- 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);
7831
7944
  canonical = upsertSection(canonical, "critical-context", critical.join(`
7832
7945
  `) || "- None recorded.");
7833
7946
  }
7834
7947
  break;
7835
7948
  }
7836
7949
  case "missing-file":
7837
- canonical = appendToSection(canonical, "files-modified", "- " + gap.path);
7950
+ canonical = appendToSection(canonical, "files-modified", "- " + safe(gap.path));
7838
7951
  break;
7839
7952
  case "missing-error": {
7840
7953
  const existing = findSection(canonical, "critical-context")?.body.toLowerCase() ?? "";
7841
- const message = gap.message.slice(0, TRUNC.MESSAGE);
7954
+ const message = safe(gap.message);
7842
7955
  if (!existing.includes(message.toLowerCase())) {
7843
7956
  canonical = appendToSection(canonical, "critical-context", "- Unresolved error: " + message);
7844
7957
  }
7845
7958
  break;
7846
7959
  }
7847
7960
  case "missing-constraint":
7848
- canonical = appendToSection(canonical, "constraints", "- " + gap.text.slice(0, TRUNC.CONSTRAINT_TEXT));
7961
+ canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
7849
7962
  break;
7850
7963
  case "missing-decision":
7851
- canonical = appendToSection(canonical, "decisions", "- **" + gap.summary.slice(0, TRUNC.DECISION_DETAIL) + "**");
7964
+ canonical = appendToSection(canonical, "decisions", "- **" + safe(gap.summary, TRUNC.DECISION_SUMMARY) + "**");
7852
7965
  break;
7853
7966
  case "missing-goal":
7854
- canonical = upsertSection(canonical, "goal", gap.goal);
7967
+ canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
7855
7968
  break;
7856
7969
  case "missing-open-loops": {
7857
- const current = extraction.errors.filter((error) => !error.resolved).map((error) => "- [high] Resolve " + error.message.slice(0, TRUNC.SNIPPET));
7858
- const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error) => "- [high] Resolve " + error.message.slice(0, TRUNC.SNIPPET));
7859
- 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);
7860
7973
  const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
7861
7974
  `);
7862
7975
  canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
@@ -7864,7 +7977,6 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7864
7977
  }
7865
7978
  case "fabricated-file": {
7866
7979
  const normalizedRef = gap.ref.replace(/\\/g, "/").toLowerCase();
7867
- let removed = false;
7868
7980
  canonical = {
7869
7981
  sections: canonical.sections.map((section) => ({
7870
7982
  ...section,
@@ -7873,42 +7985,32 @@ function patchDeterministic(summary, gaps, extraction, continuity = null) {
7873
7985
  if (!/^\s*[-*]\s+/.test(line))
7874
7986
  return true;
7875
7987
  const matches = extractFileRefs(line).some((ref) => ref.replace(/\\/g, "/").toLowerCase() === normalizedRef);
7876
- if (matches)
7877
- removed = true;
7878
7988
  return !matches;
7879
7989
  }).join(`
7880
7990
  `).trim()
7881
7991
  }))
7882
7992
  };
7883
- if (!removed)
7884
- verificationNotes.push(formatVerificationGap(gap));
7885
7993
  break;
7886
7994
  }
7887
7995
  case "inconsistency":
7888
7996
  if (gap.detail.startsWith("blocked-none:"))
7889
7997
  patchBlockedNone();
7890
- else
7891
- verificationNotes.push(formatVerificationGap(gap));
7892
7998
  break;
7893
7999
  }
7894
8000
  }
7895
- if (verificationNotes.length > 0) {
7896
- canonical = upsertSection(canonical, "verification-note", verificationNotes.map((note) => "- " + note).join(`
7897
- `));
7898
- }
7899
8001
  return renderSummary(canonical, { canonicalHeadings: true });
7900
8002
  }
7901
8003
  async function patchSummary(summary, gaps, model, auth, signal, services) {
7902
- 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.
7903
8005
 
7904
- Missing items:
8006
+ Findings:
7905
8007
  ` + gaps.map((gap, index) => index + 1 + ". " + formatVerificationGap(gap)).join(`
7906
8008
  `) + `
7907
8009
 
7908
8010
  Current summary:
7909
8011
  ` + summary + `
7910
8012
 
7911
- Return the COMPLETE updated summary with missing items integrated. Keep the same format.`;
8013
+ Return the COMPLETE corrected summary in the same format.`;
7912
8014
  try {
7913
8015
  const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
7914
8016
  const response = await trackedComplete("patch", model, {
@@ -7928,10 +8030,6 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
7928
8030
  init_overlays();
7929
8031
  init_logger();
7930
8032
  init_mode_policy();
7931
- function informationTokenCount(summary) {
7932
- const ignored = new Set(["none", "recorded", "continue", "current", "work", "explicit", "completion"]);
7933
- return new Set((summary.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) ?? []).filter((token) => !ignored.has(token))).size;
7934
- }
7935
8033
  async function verifyAndPatch(rc) {
7936
8034
  const extraction = rc.extraction;
7937
8035
  let summary = rc.finalSummary;
@@ -7948,17 +8046,20 @@ async function verifyAndPatch(rc) {
7948
8046
  }
7949
8047
  let verification = verifySummary(summary, extraction, rc.previousState);
7950
8048
  const initialScore = verification.score;
7951
- const deterministicPatched = verification.gaps.filter(isDeterministicallyPatchable);
8049
+ const deterministicPatched = [];
7952
8050
  let llmPatched = false;
7953
8051
  let qualityFloorUsed = false;
7954
8052
  rc.vlog("Verification score=" + verification.score + " ok=" + verification.ok + " gaps=" + verification.gaps.length);
7955
- if (deterministicPatched.length > 0) {
7956
- rc.notify("Phase 4 Verify: " + deterministicPatched.length + " deterministic gap(s), score=" + verification.score + ", applying repair", "warning");
7957
- summary = patchDeterministic(summary, verification.gaps, extraction, rc.previousState);
7958
- 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);
7959
8060
  }
7960
8061
  const mode = rc.mode ?? (rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced");
7961
- if (MODE_POLICIES[mode].allowLlmPatch && !verification.ok && verification.score < 75) {
8062
+ if (MODE_POLICIES[mode].allowLlmPatch && !verification.ok) {
7962
8063
  rc.notify("Phase 4 Verify: deterministic repair insufficient (score=" + verification.score + "), requesting LLM patch", "warning");
7963
8064
  const beforePatch = summary;
7964
8065
  try {
@@ -7970,34 +8071,30 @@ async function verifyAndPatch(rc) {
7970
8071
  if (summary !== beforePatch) {
7971
8072
  llmPatched = true;
7972
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);
7973
8078
  }
7974
8079
  }
7975
8080
  if (!verification.ok) {
7976
8081
  let deterministic = assembleFallback(rc.summaries, extraction);
7977
8082
  let deterministicVerification = verifySummary(deterministic, extraction, rc.previousState);
7978
- const patchable = deterministicVerification.gaps.filter(isDeterministicallyPatchable);
7979
- if (patchable.length > 0) {
7980
- deterministic = patchDeterministic(deterministic, deterministicVerification.gaps, extraction, rc.previousState);
7981
- deterministicVerification = verifySummary(deterministic, extraction, rc.previousState);
7982
- }
7983
- const gain = deterministicVerification.score - verification.score;
7984
- const currentInformation = Math.max(1, informationTokenCount(summary));
7985
- const fallbackCoverage = informationTokenCount(deterministic) / currentInformation;
7986
- const semanticSafetyFailure = verification.gaps.some((gap) => gap.kind === "inconsistency" && gap.detail.startsWith("semantic-contradiction:"));
7987
- const catastrophic = verification.score < 50;
7988
- const materiallyBetter = gain >= 15 && fallbackCoverage >= 0.65;
7989
- 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) {
7990
8087
  summary = deterministic;
7991
8088
  verification = deterministicVerification;
7992
- deterministicPatched.push(...patchable);
8089
+ deterministicPatched.push(...repaired.patched);
7993
8090
  qualityFloorUsed = true;
7994
- 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");
7995
8092
  }
7996
8093
  }
7997
8094
  const failure = verificationFailureMessage(verification);
7998
8095
  if (failure) {
7999
8096
  rc.notify(failure + " \u2014 current conversation left unchanged", "error");
8000
- throw new Error(failure);
8097
+ throw new VerificationGateError(verification, initialScore);
8001
8098
  }
8002
8099
  const out = rc;
8003
8100
  out.finalSummary = summary;
@@ -8072,24 +8169,23 @@ function buildState(rc) {
8072
8169
  summary = rc.services.scrubber.scrubText(summary).value;
8073
8170
  compactionState = rc.services.scrubber.scrubValue(compactionState).value;
8074
8171
  let postVerification = verifySummary(summary, extraction, compactionState);
8075
- const postPatchable = postVerification.gaps.filter(isDeterministicallyPatchable);
8076
- if (postPatchable.length > 0) {
8077
- summary = patchDeterministic(summary, postVerification.gaps, extraction, compactionState);
8078
- postVerification = verifySummary(summary, extraction, compactionState);
8079
- }
8172
+ const postInitialScore = postVerification.score;
8173
+ const postRepair = repairSummaryDeterministically(summary, postVerification, extraction, compactionState);
8174
+ summary = postRepair.summary;
8175
+ postVerification = postRepair.result;
8080
8176
  rc.verified = postVerification.ok;
8081
8177
  rc.verificationScore = postVerification.score;
8082
8178
  rc.verificationGaps = postVerification.gaps.map(formatVerificationGap);
8083
8179
  rc.verificationProvenance = {
8084
8180
  ...rc.verificationProvenance,
8085
- deterministicPatched: [...rc.verificationProvenance.deterministicPatched, ...postPatchable],
8181
+ deterministicPatched: [...rc.verificationProvenance.deterministicPatched, ...postRepair.patched],
8086
8182
  finalScore: postVerification.score,
8087
8183
  remainingGaps: postVerification.gaps
8088
8184
  };
8089
8185
  const failure = verificationFailureMessage(postVerification);
8090
8186
  if (failure) {
8091
8187
  rc.notify(failure + " after continuity injection \u2014 current conversation left unchanged", "error");
8092
- throw new Error(failure);
8188
+ throw new VerificationGateError(postVerification, postInitialScore);
8093
8189
  }
8094
8190
  const detModified = extraction.modifiedFiles.map((f) => f.path);
8095
8191
  const detRead = extraction.readFiles;
@@ -8153,11 +8249,37 @@ var MAX_MANUAL_NODES = 500;
8153
8249
  var MAX_SESSION_NODES = 256;
8154
8250
  var MAX_QUERY_CANDIDATES = 80;
8155
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
+ }
8156
8272
  function openDatabase() {
8157
8273
  const fp = contextGraphFile();
8158
8274
  fs7.mkdirSync(path10.dirname(fp), { recursive: true });
8159
- const { Database } = require2("bun:sqlite");
8160
- 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
+ }
8161
8283
  try {
8162
8284
  fs7.chmodSync(fp, 384);
8163
8285
  } catch {}
@@ -8824,6 +8946,22 @@ function recordSuccessMetrics(rc, status) {
8824
8946
  function recordFailureMetrics(rc, err, fields) {
8825
8947
  const releaseChannel = rc.config?.telemetryChannel ?? loadConfig().telemetryChannel;
8826
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;
8827
8965
  appendMetricsLog(fields.sessionId ?? "unknown", {
8828
8966
  runId: rc.runId,
8829
8967
  metricsSchemaVersion: 2,
@@ -8843,6 +8981,11 @@ function recordFailureMetrics(rc, err, fields) {
8843
8981
  runType: runType(rc),
8844
8982
  status: rc.cancellation.timedOut ? "timeout" : "error",
8845
8983
  fallbackReason: "failure:" + failureKind,
8984
+ verificationScore,
8985
+ initialVerificationScore,
8986
+ verificationGaps,
8987
+ remainingVerificationGaps: verificationGaps,
8988
+ verificationGapKinds: gapKinds,
8846
8989
  phaseTimings: rc.phaseTimings,
8847
8990
  durationMs: Date.now() - rc.pipelineStart
8848
8991
  }, rc.services);
@@ -8978,7 +9121,8 @@ function makeBase(opts) {
8978
9121
  dryRun: !!opts.dryRun,
8979
9122
  autoTriggered: !!opts.autoTriggered,
8980
9123
  skipCompact: !!opts.skipCompact,
8981
- force: !!opts.force
9124
+ force: !!opts.force,
9125
+ overflowRecovery: !!opts.overflowRecovery
8982
9126
  },
8983
9127
  userNote: opts.userNote,
8984
9128
  focus: opts.focus,
@@ -9004,8 +9148,11 @@ async function runSmartCompact(opts) {
9004
9148
  return;
9005
9149
  }
9006
9150
  const runSessionId = resolveSessionId(opts.ctx);
9007
- 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");
9008
9154
  return;
9155
+ }
9009
9156
  const base = makeBase(opts);
9010
9157
  const abortFromHost = () => {
9011
9158
  base.cancellation.timedOut = true;
@@ -9775,8 +9922,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9775
9922
  const totalTokens = usage?.tokens ?? 0;
9776
9923
  const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
9777
9924
  if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
9778
- ctx.ui.notify("Context OK or unknown", "info");
9779
- 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");
9780
9926
  }
9781
9927
  const cur = ctx.model;
9782
9928
  const avail = ctx.modelRegistry.getAvailable();
@@ -9845,7 +9991,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9845
9991
  if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
9846
9992
  return;
9847
9993
  const pct = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
9848
- if (pct < config.minContextPercent)
9994
+ if (event.reason !== "overflow" && pct < config.minContextPercent)
9849
9995
  return;
9850
9996
  const cur = ctx.model;
9851
9997
  if (!cur)
@@ -9874,6 +10020,7 @@ Paths: ` + relatedPaths.join(", ") : ""));
9874
10020
  isRunning,
9875
10021
  onNativeApplyError,
9876
10022
  autoTriggered: true,
10023
+ overflowRecovery: event.reason === "overflow",
9877
10024
  timeoutMs: effectiveTimeoutMs,
9878
10025
  cancellationOut
9879
10026
  });