pi-smart-compact 9.6.1 → 9.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ var __require = import.meta.require;
3
3
 
4
4
  // src/index.ts
5
5
  import {
6
- convertToLlm as convertToLlm4
6
+ convertToLlm as convertToLlm5
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
 
9
9
  // src/constants.ts
10
- var VERSION = "9.6.1";
10
+ var VERSION = "9.6.2";
11
11
  var CHARS_PER_TOKEN = 3.8;
12
12
  var MIN_COMPACTION_SAVING_RATIO = 0.1;
13
13
  var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
@@ -1861,16 +1861,8 @@ function findLastAnchorIndex(branchEntries) {
1861
1861
  return -1;
1862
1862
  }
1863
1863
  function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
1864
- let msgCount = 0;
1865
- for (let i = 0;i <= branchIdx && i < branchEntries.length; i++) {
1866
- const e = branchEntries[i];
1867
- if (e?.type === "message") {
1868
- if (msgCount >= msgs.length)
1869
- return msgs.length - 1;
1870
- msgCount++;
1871
- }
1872
- }
1873
- return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
1864
+ const id = branchEntries[branchIdx]?.id;
1865
+ return Math.max(0, msgs.findIndex((entry) => entry.id === id));
1874
1866
  }
1875
1867
  function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
1876
1868
  const candidates = [];
@@ -3816,6 +3808,190 @@ function getDefaultServices() {
3816
3808
  return _default;
3817
3809
  }
3818
3810
 
3811
+ // src/domain/telemetry.ts
3812
+ function errorFields(error2, seen = new Set) {
3813
+ if (!error2 || typeof error2 !== "object") {
3814
+ return { name: "", message: String(error2 ?? ""), status: null, code: "" };
3815
+ }
3816
+ if (seen.has(error2) || seen.size >= 8)
3817
+ return { name: "", message: "", status: null, code: "" };
3818
+ seen.add(error2);
3819
+ const value = error2;
3820
+ const cause = value.cause ? errorFields(value.cause, seen) : null;
3821
+ const numericStatus = Number(value.status ?? value.statusCode);
3822
+ return {
3823
+ name: typeof value.name === "string" ? value.name : cause?.name ?? "",
3824
+ message: (typeof value.message === "string" ? value.message : "") + (cause?.message ? " " + cause.message : ""),
3825
+ status: Number.isFinite(numericStatus) ? numericStatus : cause?.status ?? null,
3826
+ code: typeof value.code === "string" ? value.code : cause?.code ?? ""
3827
+ };
3828
+ }
3829
+ function classifyTelemetryFailure(error2, timedOut = false) {
3830
+ const fields = errorFields(error2);
3831
+ const text = (fields.name + " " + fields.code + " " + fields.message).toLowerCase();
3832
+ if (timedOut)
3833
+ return "timeout";
3834
+ if (/max(?:imum)? output|output.?limit|visible[ -]output|length limit/.test(text))
3835
+ return "output-limit";
3836
+ if (/timeout|timed out|watchdog|deadline/.test(text))
3837
+ return "timeout";
3838
+ if (fields.name.toLowerCase() === "verificationgateerror")
3839
+ return "verification";
3840
+ if (fields.name.toLowerCase() === "yieldgateerror")
3841
+ return "yield";
3842
+ if (/budgetexceeded|token budget|call budget|latency budget/.test(text))
3843
+ return "budget";
3844
+ if (fields.status === 429 || /rate.?limit|too many requests|quota/.test(text))
3845
+ return "rate-limit";
3846
+ if (fields.status === 401 || fields.status === 403 || /unauthori[sz]ed|authentication|api.?key|credential/.test(text))
3847
+ return "authentication";
3848
+ if (/abort|cancel/.test(text))
3849
+ return "cancelled";
3850
+ if (/native compaction|persist|write|rename|filesystem|sqlite|database/.test(text))
3851
+ return "persistence";
3852
+ if (/verificationgateerror|verification gate|verification.*(?:gap|summary)/.test(text))
3853
+ return "verification";
3854
+ if (/invalid|validation|schema|malformed|required/.test(text))
3855
+ return "validation";
3856
+ if (fields.status != null && fields.status >= 500 || /provider|api error|stream|network|fetch failed|socket/.test(text))
3857
+ return "provider";
3858
+ return "internal";
3859
+ }
3860
+ function p95(values) {
3861
+ if (!values.length)
3862
+ return 0;
3863
+ const sorted = [...values].sort((a, b) => a - b);
3864
+ return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
3865
+ }
3866
+ function stats(entries, damage) {
3867
+ const evidence = entries.filter((entry) => entry.status !== "dry-run");
3868
+ const successfulRuns = evidence.filter((entry) => entry.status === "success");
3869
+ const quality = successfulRuns.filter((entry) => typeof entry.verificationScore === "number");
3870
+ const appliedRunIds = new Set(successfulRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
3871
+ const observedScores = new Map;
3872
+ for (const observation of damage) {
3873
+ if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
3874
+ continue;
3875
+ observedScores.set(observation.runId, Math.max(observedScores.get(observation.runId) ?? 0, Math.max(0, Math.min(100, observation.damageScore))));
3876
+ }
3877
+ const damaging = [...observedScores.values()].filter((score) => score > 0).length;
3878
+ return {
3879
+ runs: entries.length,
3880
+ appliedRuns: evidence.length,
3881
+ successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
3882
+ avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
3883
+ qualityCoverage: evidence.length ? quality.length / evidence.length : 0,
3884
+ p95LatencyMs: p95(evidence.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
3885
+ avgTokens: evidence.length ? evidence.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / evidence.length : 0,
3886
+ fallbackRate: evidence.length ? evidence.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / evidence.length : 0,
3887
+ damageRate: observedScores.size ? damaging / observedScores.size : 0,
3888
+ damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
3889
+ };
3890
+ }
3891
+ function roundStats(value) {
3892
+ return {
3893
+ ...value,
3894
+ successRate: Math.round(value.successRate * 1000) / 1000,
3895
+ avgQuality: value.avgQuality == null ? null : Math.round(value.avgQuality * 10) / 10,
3896
+ qualityCoverage: Math.round(value.qualityCoverage * 1000) / 1000,
3897
+ p95LatencyMs: Math.round(value.p95LatencyMs),
3898
+ avgTokens: Math.round(value.avgTokens),
3899
+ fallbackRate: Math.round(value.fallbackRate * 1000) / 1000,
3900
+ damageRate: Math.round(value.damageRate * 1000) / 1000,
3901
+ damageCoverage: Math.round(value.damageCoverage * 1000) / 1000
3902
+ };
3903
+ }
3904
+ function assessCanary(entries, damageEntries, options) {
3905
+ const minCanaryRuns = Math.max(5, options.minCanaryRuns ?? 20);
3906
+ const canaryEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && entry.version === options.version && entry.releaseChannel === "canary").slice(-Math.max(100, minCanaryRuns));
3907
+ const baselineEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && (entry.releaseChannel ?? "stable") === "stable").slice(-(options.baselineRuns ?? Math.max(50, minCanaryRuns * 2)));
3908
+ const baseline = stats(baselineEntries, damageEntries);
3909
+ const canary = stats(canaryEntries, damageEntries);
3910
+ const triggers = [];
3911
+ const failureBaseline = 1 - baseline.successRate;
3912
+ const failureCanary = 1 - canary.successRate;
3913
+ if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
3914
+ triggers.push({
3915
+ metric: "failure-rate",
3916
+ baseline: failureBaseline,
3917
+ canary: failureCanary,
3918
+ threshold: failureCanary > 0.050001 ? ">5% absolute" : "+5pp regression"
3919
+ });
3920
+ }
3921
+ if (canary.avgQuality != null && (canary.avgQuality < 85 || baseline.avgQuality != null && baseline.avgQuality - canary.avgQuality >= 5)) {
3922
+ triggers.push({
3923
+ metric: "quality",
3924
+ baseline: baseline.avgQuality ?? 0,
3925
+ canary: canary.avgQuality,
3926
+ threshold: canary.avgQuality < 85 ? "<85 absolute" : "-5 points"
3927
+ });
3928
+ }
3929
+ if (baseline.p95LatencyMs >= 1000 && canary.p95LatencyMs >= baseline.p95LatencyMs * 1.5) {
3930
+ triggers.push({ metric: "latency", baseline: baseline.p95LatencyMs, canary: canary.p95LatencyMs, threshold: "+50% p95" });
3931
+ }
3932
+ if (baseline.avgTokens >= 1000 && canary.avgTokens >= baseline.avgTokens * 1.5) {
3933
+ triggers.push({ metric: "tokens", baseline: baseline.avgTokens, canary: canary.avgTokens, threshold: "+50%" });
3934
+ }
3935
+ if (canary.fallbackRate - baseline.fallbackRate >= 0.1) {
3936
+ triggers.push({ metric: "fallback", baseline: baseline.fallbackRate, canary: canary.fallbackRate, threshold: "+10pp" });
3937
+ }
3938
+ if (canary.damageRate - baseline.damageRate >= 0.1) {
3939
+ triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
3940
+ }
3941
+ const canarySampleAdequacy = Math.min(1, canary.appliedRuns / minCanaryRuns);
3942
+ const baselineSampleAdequacy = Math.min(1, baseline.appliedRuns / Math.max(20, minCanaryRuns));
3943
+ const dataConfidence = Math.round(100 * (canarySampleAdequacy * 0.25 + baselineSampleAdequacy * 0.15 + canary.qualityCoverage * canarySampleAdequacy * 0.2 + canary.damageCoverage * canarySampleAdequacy * 0.2 + baseline.damageCoverage * baselineSampleAdequacy * 0.2));
3944
+ const reasons = [];
3945
+ let decision = "hold";
3946
+ if (triggers.length && canary.appliedRuns >= 3) {
3947
+ decision = "rollback";
3948
+ reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
3949
+ } else if (canary.appliedRuns < minCanaryRuns) {
3950
+ reasons.push("need " + (minCanaryRuns - canary.appliedRuns) + " more canary runs with applied outcomes");
3951
+ } else if (baseline.appliedRuns < Math.max(20, minCanaryRuns)) {
3952
+ reasons.push("stable baseline is too small");
3953
+ } else if (canary.qualityCoverage < 0.7) {
3954
+ reasons.push("schema-v2 quality coverage is below 70%");
3955
+ } else if (canary.damageCoverage < 0.7) {
3956
+ reasons.push("correlated canary damage-observation coverage is below 70%");
3957
+ } else if (baseline.damageCoverage < 0.7) {
3958
+ reasons.push("correlated stable damage-observation coverage is below 70%");
3959
+ } else if ((canary.avgQuality ?? 0) < 85) {
3960
+ reasons.push("absolute verifier quality is below 85");
3961
+ } else if (canary.successRate < 0.949999) {
3962
+ reasons.push("absolute success rate is below 95%");
3963
+ } else {
3964
+ decision = "promote";
3965
+ reasons.push("sample, absolute quality, reliability, latency, token, fallback, and damage gates passed");
3966
+ }
3967
+ return {
3968
+ version: options.version,
3969
+ decision,
3970
+ dataConfidence,
3971
+ baseline: roundStats(baseline),
3972
+ canary: roundStats(canary),
3973
+ triggers,
3974
+ reasons
3975
+ };
3976
+ }
3977
+ var TELEMETRY_FAILURE_KINDS = new Set([
3978
+ "cancelled",
3979
+ "timeout",
3980
+ "rate-limit",
3981
+ "authentication",
3982
+ "budget",
3983
+ "output-limit",
3984
+ "provider",
3985
+ "persistence",
3986
+ "validation",
3987
+ "verification",
3988
+ "yield",
3989
+ "internal"
3990
+ ]);
3991
+ function isTelemetryFailureKind(value) {
3992
+ return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
3993
+ }
3994
+
3819
3995
  // src/utils/cache.ts
3820
3996
  var INTERNAL_PHASES = new Set([
3821
3997
  "explore-retry",
@@ -3929,7 +4105,8 @@ async function trackedComplete(phase, model, reqBody, opts, services) {
3929
4105
  cacheHitTokens: 0,
3930
4106
  cacheWriteTokens: 0,
3931
4107
  latencyMs: Date.now() - start,
3932
- success: false
4108
+ success: false,
4109
+ failureKind: classifyTelemetryFailure(err)
3933
4110
  }, svc);
3934
4111
  throw err;
3935
4112
  }
@@ -4537,11 +4714,17 @@ function batchOutputLimit(mode, chunks, providerMax) {
4537
4714
  const budget = MODE_POLICIES[mode].batchOutput;
4538
4715
  return Math.min(Math.max(budget.min, chunks * budget.perChunk), budget.max, providerMax);
4539
4716
  }
4540
- function effectiveBudget(configured, modeDefault) {
4717
+ function effectiveBudget(configured, modeDefault, override) {
4718
+ if (override !== undefined && override > 0)
4719
+ return override;
4541
4720
  if (configured <= 0)
4542
4721
  return modeDefault;
4543
4722
  return Math.min(configured, modeDefault);
4544
4723
  }
4724
+ function resolveCallBudget(configured, mode, override, automatic = false) {
4725
+ const budget = effectiveBudget(configured, MODE_POLICIES[mode].maxLlmCalls, override);
4726
+ return automatic ? Math.min(budget, AUTO_TRIGGER_MAX_LLM_CALLS) : budget;
4727
+ }
4545
4728
 
4546
4729
  // src/ui/overlays.ts
4547
4730
  import { DynamicBorder as DynamicBorder2 } from "@earendil-works/pi-coding-agent";
@@ -5020,6 +5203,27 @@ function readRemediationHints(projectId) {
5020
5203
  return data.files.filter((f) => typeof f === "string");
5021
5204
  }
5022
5205
 
5206
+ // src/infra/ai-messages.ts
5207
+ import { contentText } from "@earendil-works/pi-ai";
5208
+ import { convertToLlm, sessionEntryToContextMessages, serializeConversation } from "@earendil-works/pi-coding-agent";
5209
+ function contextMessageEntries(entries) {
5210
+ return entries.flatMap((entry) => convertToLlm(sessionEntryToContextMessages(entry)).map((message) => ({ type: "message", id: entry.id, message })));
5211
+ }
5212
+ function serializeConversationText(messages) {
5213
+ return asSerializableMessages(messages).map((message) => message.role === "toolResult" ? "[Tool result]: " + contentText(message.content, "") : serializeConversation([message])).filter(Boolean).join(`
5214
+
5215
+ `);
5216
+ }
5217
+ function asBranchMessage(message) {
5218
+ return message;
5219
+ }
5220
+ function asSerializableMessages(msgs) {
5221
+ return msgs;
5222
+ }
5223
+ function scrubLlmMessages(msgs, scrubber) {
5224
+ return scrubber.scrubValue(msgs).value;
5225
+ }
5226
+
5023
5227
  // src/app/run-context.ts
5024
5228
  function markMeasuredPhase(rc, phase, startMs, endMs = Date.now()) {
5025
5229
  rc.phaseTimings.push({ phase, durationMs: Math.max(0, endMs - startMs) });
@@ -5242,7 +5446,7 @@ function resolveCompactionWindow(rc) {
5242
5446
  const totalTokens = rc.ctx.getContextUsage()?.tokens ?? 0;
5243
5447
  const manager = rc.ctx.sessionManager;
5244
5448
  const branch = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : manager.getBranch();
5245
- const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
5449
+ const msgs = contextMessageEntries(branch);
5246
5450
  if (msgs.length < 3) {
5247
5451
  if (rc.flags.force)
5248
5452
  rc.notify("Manual compaction skipped: fewer than 3 active messages are available.", "warning");
@@ -5328,12 +5532,12 @@ function preparePreflightProfile(input) {
5328
5532
  }
5329
5533
  function prepareManualPreflightContext(ctx, summaryModel, tokenCalibration) {
5330
5534
  const branch = typeof ctx.sessionManager.buildContextEntries === "function" ? ctx.sessionManager.buildContextEntries() : ctx.sessionManager.getBranch();
5331
- const msgs = branch.filter((entry) => entry.type === "message" && entry.message != null);
5535
+ const msgs = contextMessageEntries(branch);
5332
5536
  const totalTokens = ctx.getContextUsage()?.tokens ?? 0;
5333
5537
  const modelContextWindow = ctx.model?.contextWindow;
5334
5538
  const contextWindowTokens = Number.isFinite(modelContextWindow) && (modelContextWindow ?? 0) > 0 ? modelContextWindow : 0;
5335
5539
  const contextPercent = safeContextPercent(totalTokens, modelContextWindow);
5336
- const toolPercent = computeToolCharPercentage(branch);
5540
+ const toolPercent = computeToolCharPercentage(msgs);
5337
5541
  const overflowedContext = contextWindowTokens > 0 && totalTokens > contextWindowTokens;
5338
5542
  const estimator = makeTokenEstimator(summaryModel.provider, summaryModel.id, tokenCalibration);
5339
5543
  const messageTokens = msgs.map((entry) => estimator.message(entry.message));
@@ -6222,7 +6426,7 @@ function formatPreflightSummary(preflight, modelLabel, details = false) {
6222
6426
  lines2.push("Estimator messages ~" + tokenCount(preflight.rawEstimatedMessageTokens) + " \xB7 normalization unavailable", "Route " + modelLabel + " \xB7 viability " + preflight.reason);
6223
6427
  return lines2;
6224
6428
  }
6225
- const stateReserve = Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO);
6429
+ const stateReserve = Math.max(0, (plan.finalSummaryAllowanceTokens ?? plan.summaryBudgetTokens + Math.ceil(plan.summaryBudgetTokens * POST_SUMMARY_RESERVE_RATIO)) - plan.summaryBudgetTokens);
6226
6430
  const lines = [
6227
6431
  "Plan " + compactTokenCount(preflight.totalTokens) + " \u2192 ~" + compactTokenCount(plan.projectedAfterTokens) + " \xB7 ~" + compactTokenCount(plan.projectedSavedTokens) + " saved (" + percent(plan.projectedYield * 100) + ")",
6228
6432
  "Keep ~" + compactTokenCount(plan.retainedTokens) + " recent \xB7 summary up to " + compactTokenCount(plan.summaryBudgetTokens) + " + ~" + compactTokenCount(stateReserve) + " verified-state reserve",
@@ -6512,8 +6716,8 @@ async function showCompactUI(ctx, opts) {
6512
6716
  const marker = index === selected ? "\u203A " : " ";
6513
6717
  const recommendedMark = mode === recommended.mode ? " recommended" : " ";
6514
6718
  const trait = mode === "fast" ? "quickest" : mode === "balanced" ? "default" : "deepest";
6515
- const stats = (viable && plan ? "~" + compactTokenCount(plan.projectedAfterTokens) + " after \xB7 " + percent(plan.projectedYield * 100) + " saved" : "unavailable \xB7 " + explainPreflightReason(preview.reason)) + " \xB7 " + trait;
6516
- const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " + stats;
6719
+ const stats2 = (viable && plan ? "~" + compactTokenCount(plan.projectedAfterTokens) + " after \xB7 " + percent(plan.projectedYield * 100) + " saved" : "unavailable \xB7 " + explainPreflightReason(preview.reason)) + " \xB7 " + trait;
6720
+ const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " + stats2;
6517
6721
  lines.push(cell(index === selected ? theme.fg("accent", theme.bold(line)) : theme.fg(viable ? mode === recommended.mode ? "success" : "text" : "muted", line)));
6518
6722
  }
6519
6723
  lines.push(divider);
@@ -6606,8 +6810,8 @@ async function prepareRun(rc) {
6606
6810
  rc.timeoutMs = rc.timeoutMs > 0 ? Math.min(rc.timeoutMs, config.maxLatencyMs) : config.maxLatencyMs;
6607
6811
  }
6608
6812
  const policy = MODE_POLICIES[rc.mode];
6609
- const callBudget = rc.maxLlmCalls ?? effectiveBudget(config.maxLlmCalls, policy.maxLlmCalls);
6610
- const inputBudget = rc.maxLlmInputTokens ?? effectiveBudget(config.maxLlmInputTokens, policy.maxInputTokens);
6813
+ const callBudget = resolveCallBudget(config.maxLlmCalls, rc.mode, rc.maxLlmCalls, rc.flags.autoTriggered && !rc.flags.skipCompact);
6814
+ const inputBudget = effectiveBudget(config.maxLlmInputTokens, policy.maxInputTokens, rc.maxLlmInputTokens);
6611
6815
  rc.services.budget = new BudgetGuard(callBudget, rc.timeoutMs, rc.services.clock, inputBudget, policy.maxOutputTokens);
6612
6816
  if (rc.timeoutMs > 0) {
6613
6817
  rc.cancellation.timeoutId = setTimeout(() => {
@@ -6627,24 +6831,13 @@ async function prepareRun(rc) {
6627
6831
  }
6628
6832
 
6629
6833
  // src/app/steps/recover.ts
6630
- import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
6631
-
6632
- // src/infra/ai-messages.ts
6633
- function asBranchMessage(message) {
6634
- return message;
6635
- }
6636
- function asSerializableMessages(msgs) {
6637
- return msgs;
6638
- }
6639
- function scrubLlmMessages(msgs, scrubber) {
6640
- return scrubber.scrubValue(msgs).value;
6641
- }
6834
+ import { convertToLlm as convertToLlm3 } from "@earendil-works/pi-coding-agent";
6642
6835
 
6643
6836
  // src/utils/session-log.ts
6644
6837
  import * as fs6 from "fs";
6645
6838
  import * as path10 from "path";
6646
6839
  import { StringDecoder } from "string_decoder";
6647
- import { convertToLlm } from "@earendil-works/pi-coding-agent";
6840
+ import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
6648
6841
  function getSessionsDir() {
6649
6842
  return sessionsDir();
6650
6843
  }
@@ -6779,10 +6972,10 @@ async function readOriginalMessageMap(sessionId, wantedIds, cwd) {
6779
6972
  } catch {
6780
6973
  continue;
6781
6974
  }
6782
- if (entry.type !== "message" || !entry.id || !remaining.has(entry.id) || !entry.message)
6975
+ if (!entry.id || !remaining.has(entry.id))
6783
6976
  continue;
6784
6977
  remaining.delete(entry.id);
6785
- const normalized = normalizeLogMessage(entry.message, entry.timestamp);
6978
+ const normalized = entry.type === "message" && entry.message ? normalizeLogMessage(entry.message, entry.timestamp) : contextMessageEntries([entry])[0]?.message;
6786
6979
  if (normalized)
6787
6980
  map.set(entry.id, normalized);
6788
6981
  if (remaining.size === 0)
@@ -6812,7 +7005,7 @@ async function resolveCompactionMessages(sessionId, toCompactEntries, cwd) {
6812
7005
  for (const entry of toCompactEntries) {
6813
7006
  if (!entry.id)
6814
7007
  continue;
6815
- const converted = convertToLlm([
7008
+ const converted = convertToLlm2([
6816
7009
  asBranchMessage(entry.message)
6817
7010
  ]);
6818
7011
  if (!converted.length)
@@ -6837,7 +7030,7 @@ async function recoverSessionLog(rc) {
6837
7030
  let resolved = rc.toCompact.flatMap((entry) => {
6838
7031
  if (!entry.id)
6839
7032
  return [];
6840
- return convertToLlm2([asBranchMessage(entry.message)]).map((message) => ({ entryId: entry.id, message }));
7033
+ return convertToLlm3([asBranchMessage(entry.message)]).map((message) => ({ entryId: entry.id, message }));
6841
7034
  });
6842
7035
  if (hasTruncatedMessages(resolved.map((item) => item.message))) {
6843
7036
  const fromLog = await resolveCompactionMessages(rc.sessionId, rc.toCompact, rc.ctx.cwd);
@@ -6854,7 +7047,7 @@ async function recoverSessionLog(rc) {
6854
7047
 
6855
7048
  // src/app/steps/tier.ts
6856
7049
  function selectTier(rc) {
6857
- const toolPercent = computeToolCharPercentage(rc.branch);
7050
+ const toolPercent = computeToolCharPercentage(rc.msgs);
6858
7051
  const tier = rc.flags.overflowRecovery ? "full" : rc.flags.force ? rc.contextPercent >= 80 ? "full" : "light" : selectCompactionTier(rc.contextPercent, rc.totalTokens, MIN_TOKEN_THRESHOLD, rc.config.minContextPercent);
6859
7052
  if (tier === "none") {
6860
7053
  if (!rc.flags.autoTriggered) {
@@ -7021,9 +7214,6 @@ function pruneRedundant(msgs, precomputedTcIdx) {
7021
7214
  };
7022
7215
  }
7023
7216
 
7024
- // src/app/steps/extract.ts
7025
- import { serializeConversation } from "@earendil-works/pi-coding-agent";
7026
-
7027
7217
  // src/utils/backups.ts
7028
7218
  import fs7 from "fs";
7029
7219
  import path11 from "path";
@@ -7249,7 +7439,7 @@ function extractWithCache(rc) {
7249
7439
  const pruneEnd = Date.now();
7250
7440
  markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
7251
7441
  const extractionStart = pruneEnd;
7252
- const convText = rc.services.scrubber.scrubText(serializeConversation(asSerializableMessages(rc.llmMessages))).value;
7442
+ const convText = rc.services.scrubber.scrubText(serializeConversationText(rc.llmMessages)).value;
7253
7443
  const convTokens = rc.estimator.text(convText);
7254
7444
  let preparedBackup;
7255
7445
  if (rc.config.backupEnabled) {
@@ -7257,7 +7447,7 @@ function extractWithCache(rc) {
7257
7447
  if (pruningUnchanged)
7258
7448
  return convText;
7259
7449
  const safeMessages = scrubLlmMessages(selectedMessages, rc.services.scrubber);
7260
- const backupText = serializeConversation(asSerializableMessages(safeMessages));
7450
+ const backupText = serializeConversationText(safeMessages);
7261
7451
  const scrubbed = rc.services.scrubber.scrubText(backupText);
7262
7452
  if (scrubbed.findings.length > 0) {
7263
7453
  rc.notify("Backup written with redactions (" + scrubbed.findings.map((f) => f.count + "x " + f.kind).join(", ") + ") \u2014 restore will lack that data", "info");
@@ -7364,207 +7554,26 @@ function extractWithCache(rc) {
7364
7554
 
7365
7555
  // src/phases/explore.ts
7366
7556
  import { Type } from "typebox";
7367
-
7368
- // src/domain/telemetry.ts
7369
- function errorFields(error2) {
7370
- if (!error2 || typeof error2 !== "object") {
7371
- return { name: "", message: String(error2 ?? ""), status: null, code: "" };
7557
+ function explicitlyRejectsTools(error2) {
7558
+ if (!error2 || typeof error2 !== "object")
7559
+ return false;
7560
+ const record = error2;
7561
+ const status = Number(record.status ?? record.statusCode ?? record.response?.status);
7562
+ const message = String(record.message ?? "");
7563
+ return (status === 400 || status === 404 || status === 422) && /(?:(?:tools?|function(?:[ -]calling)?).{0,60}(?:unsupported|not supported|unknown|unavailable|invalid)|(?:unsupported|does not support|doesn't support).{0,60}(?:tools?|function))/i.test(message);
7564
+ }
7565
+ function shouldExplore(extraction) {
7566
+ const unresolvedErrors = extraction.errors.filter((e) => !e.resolved).length;
7567
+ const topicCount = extraction.topics.length;
7568
+ const decisionCount = extraction.decisions.length;
7569
+ const crossFileWork = new Set(extraction.modifiedFiles.map((f) => {
7570
+ const parts = f.path.split("/");
7571
+ return parts.length > 1 ? parts.slice(0, -1).join("/") : "root";
7572
+ })).size;
7573
+ if (topicCount <= 3 && unresolvedErrors <= 1 && decisionCount <= 2 && crossFileWork <= 2) {
7574
+ return false;
7372
7575
  }
7373
- const value = error2;
7374
- const cause = value.cause && value.cause !== error2 ? errorFields(value.cause) : null;
7375
- const numericStatus = Number(value.status ?? value.statusCode);
7376
- return {
7377
- name: typeof value.name === "string" ? value.name : cause?.name ?? "",
7378
- message: (typeof value.message === "string" ? value.message : "") + (cause?.message ? " " + cause.message : ""),
7379
- status: Number.isFinite(numericStatus) ? numericStatus : cause?.status ?? null,
7380
- code: typeof value.code === "string" ? value.code : cause?.code ?? ""
7381
- };
7382
- }
7383
- function classifyTelemetryFailure(error2, timedOut = false) {
7384
- const fields = errorFields(error2);
7385
- const text = (fields.name + " " + fields.code + " " + fields.message).toLowerCase();
7386
- if (timedOut || /timeout|timed out|watchdog|deadline/.test(text))
7387
- return "timeout";
7388
- if (fields.name.toLowerCase() === "verificationgateerror")
7389
- return "verification";
7390
- if (fields.name.toLowerCase() === "yieldgateerror")
7391
- return "yield";
7392
- if (/budgetexceeded|token budget|call budget|latency budget/.test(text))
7393
- return "budget";
7394
- if (fields.status === 429 || /rate.?limit|too many requests|quota/.test(text))
7395
- return "rate-limit";
7396
- if (fields.status === 401 || fields.status === 403 || /unauthori[sz]ed|authentication|api.?key|credential/.test(text))
7397
- return "authentication";
7398
- if (/max(?:imum)? output|output.?limit|visible output|length limit/.test(text))
7399
- return "output-limit";
7400
- if (/abort|cancel/.test(text))
7401
- return "cancelled";
7402
- if (/native compaction|persist|write|rename|filesystem|sqlite|database/.test(text))
7403
- return "persistence";
7404
- if (/verificationgateerror|verification gate|verification.*(?:gap|summary)/.test(text))
7405
- return "verification";
7406
- if (/invalid|validation|schema|malformed|required/.test(text))
7407
- return "validation";
7408
- if (fields.status != null && fields.status >= 500 || /provider|api error|stream|network|fetch failed|socket/.test(text))
7409
- return "provider";
7410
- return "internal";
7411
- }
7412
- function p95(values) {
7413
- if (!values.length)
7414
- return 0;
7415
- const sorted = [...values].sort((a, b) => a - b);
7416
- return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
7417
- }
7418
- function stats(entries, damage) {
7419
- const evidence = entries.filter((entry) => entry.status !== "dry-run");
7420
- const successfulRuns = evidence.filter((entry) => entry.status === "success");
7421
- const quality = successfulRuns.filter((entry) => typeof entry.verificationScore === "number");
7422
- const appliedRunIds = new Set(successfulRuns.filter((entry) => typeof entry.runId === "string" && entry.runId.length >= 8).map((entry) => entry.runId));
7423
- const observedScores = new Map;
7424
- for (const observation of damage) {
7425
- if (!observation.runId || !appliedRunIds.has(observation.runId) || typeof observation.damageScore !== "number" || !Number.isFinite(observation.damageScore))
7426
- continue;
7427
- observedScores.set(observation.runId, Math.max(observedScores.get(observation.runId) ?? 0, Math.max(0, Math.min(100, observation.damageScore))));
7428
- }
7429
- const damaging = [...observedScores.values()].filter((score) => score > 0).length;
7430
- return {
7431
- runs: entries.length,
7432
- appliedRuns: evidence.length,
7433
- successRate: evidence.length ? successfulRuns.length / evidence.length : 1,
7434
- avgQuality: quality.length ? quality.reduce((sum, entry) => sum + (entry.verificationScore ?? 0), 0) / quality.length : null,
7435
- qualityCoverage: evidence.length ? quality.length / evidence.length : 0,
7436
- p95LatencyMs: p95(evidence.map((entry) => entry.durationMs ?? entry.avgLatency).filter(Number.isFinite)),
7437
- avgTokens: evidence.length ? evidence.reduce((sum, entry) => sum + entry.totalInput + entry.totalCacheHit + (entry.totalCacheWrite ?? 0) + entry.totalOutput, 0) / evidence.length : 0,
7438
- fallbackRate: evidence.length ? evidence.filter((entry) => entry.method === "heuristic" || Array.isArray(entry.providerRoutes) && entry.providerRoutes.some((route) => route.successes < route.calls)).length / evidence.length : 0,
7439
- damageRate: observedScores.size ? damaging / observedScores.size : 0,
7440
- damageCoverage: successfulRuns.length ? observedScores.size / successfulRuns.length : 0
7441
- };
7442
- }
7443
- function roundStats(value) {
7444
- return {
7445
- ...value,
7446
- successRate: Math.round(value.successRate * 1000) / 1000,
7447
- avgQuality: value.avgQuality == null ? null : Math.round(value.avgQuality * 10) / 10,
7448
- qualityCoverage: Math.round(value.qualityCoverage * 1000) / 1000,
7449
- p95LatencyMs: Math.round(value.p95LatencyMs),
7450
- avgTokens: Math.round(value.avgTokens),
7451
- fallbackRate: Math.round(value.fallbackRate * 1000) / 1000,
7452
- damageRate: Math.round(value.damageRate * 1000) / 1000,
7453
- damageCoverage: Math.round(value.damageCoverage * 1000) / 1000
7454
- };
7455
- }
7456
- function assessCanary(entries, damageEntries, options) {
7457
- const minCanaryRuns = Math.max(5, options.minCanaryRuns ?? 20);
7458
- const canaryEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && entry.version === options.version && entry.releaseChannel === "canary").slice(-Math.max(100, minCanaryRuns));
7459
- const baselineEntries = entries.filter((entry) => entry.metricsSchemaVersion === 2 && (entry.releaseChannel ?? "stable") === "stable").slice(-(options.baselineRuns ?? Math.max(50, minCanaryRuns * 2)));
7460
- const baseline = stats(baselineEntries, damageEntries);
7461
- const canary = stats(canaryEntries, damageEntries);
7462
- const triggers = [];
7463
- const failureBaseline = 1 - baseline.successRate;
7464
- const failureCanary = 1 - canary.successRate;
7465
- if (canary.appliedRuns >= 3 && (failureCanary > 0.050001 || failureCanary - failureBaseline >= 0.050001)) {
7466
- triggers.push({
7467
- metric: "failure-rate",
7468
- baseline: failureBaseline,
7469
- canary: failureCanary,
7470
- threshold: failureCanary > 0.050001 ? ">5% absolute" : "+5pp regression"
7471
- });
7472
- }
7473
- if (canary.avgQuality != null && (canary.avgQuality < 85 || baseline.avgQuality != null && baseline.avgQuality - canary.avgQuality >= 5)) {
7474
- triggers.push({
7475
- metric: "quality",
7476
- baseline: baseline.avgQuality ?? 0,
7477
- canary: canary.avgQuality,
7478
- threshold: canary.avgQuality < 85 ? "<85 absolute" : "-5 points"
7479
- });
7480
- }
7481
- if (baseline.p95LatencyMs >= 1000 && canary.p95LatencyMs >= baseline.p95LatencyMs * 1.5) {
7482
- triggers.push({ metric: "latency", baseline: baseline.p95LatencyMs, canary: canary.p95LatencyMs, threshold: "+50% p95" });
7483
- }
7484
- if (baseline.avgTokens >= 1000 && canary.avgTokens >= baseline.avgTokens * 1.5) {
7485
- triggers.push({ metric: "tokens", baseline: baseline.avgTokens, canary: canary.avgTokens, threshold: "+50%" });
7486
- }
7487
- if (canary.fallbackRate - baseline.fallbackRate >= 0.1) {
7488
- triggers.push({ metric: "fallback", baseline: baseline.fallbackRate, canary: canary.fallbackRate, threshold: "+10pp" });
7489
- }
7490
- if (canary.damageRate - baseline.damageRate >= 0.1) {
7491
- triggers.push({ metric: "damage", baseline: baseline.damageRate, canary: canary.damageRate, threshold: "+10pp" });
7492
- }
7493
- const canarySampleAdequacy = Math.min(1, canary.appliedRuns / minCanaryRuns);
7494
- const baselineSampleAdequacy = Math.min(1, baseline.appliedRuns / Math.max(20, minCanaryRuns));
7495
- const dataConfidence = Math.round(100 * (canarySampleAdequacy * 0.25 + baselineSampleAdequacy * 0.15 + canary.qualityCoverage * canarySampleAdequacy * 0.2 + canary.damageCoverage * canarySampleAdequacy * 0.2 + baseline.damageCoverage * baselineSampleAdequacy * 0.2));
7496
- const reasons = [];
7497
- let decision = "hold";
7498
- if (triggers.length && canary.appliedRuns >= 3) {
7499
- decision = "rollback";
7500
- reasons.push(...triggers.map((trigger) => trigger.metric + " crossed " + trigger.threshold));
7501
- } else if (canary.appliedRuns < minCanaryRuns) {
7502
- reasons.push("need " + (minCanaryRuns - canary.appliedRuns) + " more canary runs with applied outcomes");
7503
- } else if (baseline.appliedRuns < Math.max(20, minCanaryRuns)) {
7504
- reasons.push("stable baseline is too small");
7505
- } else if (canary.qualityCoverage < 0.7) {
7506
- reasons.push("schema-v2 quality coverage is below 70%");
7507
- } else if (canary.damageCoverage < 0.7) {
7508
- reasons.push("correlated canary damage-observation coverage is below 70%");
7509
- } else if (baseline.damageCoverage < 0.7) {
7510
- reasons.push("correlated stable damage-observation coverage is below 70%");
7511
- } else if ((canary.avgQuality ?? 0) < 85) {
7512
- reasons.push("absolute verifier quality is below 85");
7513
- } else if (canary.successRate < 0.949999) {
7514
- reasons.push("absolute success rate is below 95%");
7515
- } else {
7516
- decision = "promote";
7517
- reasons.push("sample, absolute quality, reliability, latency, token, fallback, and damage gates passed");
7518
- }
7519
- return {
7520
- version: options.version,
7521
- decision,
7522
- dataConfidence,
7523
- baseline: roundStats(baseline),
7524
- canary: roundStats(canary),
7525
- triggers,
7526
- reasons
7527
- };
7528
- }
7529
- var TELEMETRY_FAILURE_KINDS = new Set([
7530
- "cancelled",
7531
- "timeout",
7532
- "rate-limit",
7533
- "authentication",
7534
- "budget",
7535
- "output-limit",
7536
- "provider",
7537
- "persistence",
7538
- "validation",
7539
- "verification",
7540
- "yield",
7541
- "internal"
7542
- ]);
7543
- function isTelemetryFailureKind(value) {
7544
- return typeof value === "string" && TELEMETRY_FAILURE_KINDS.has(value);
7545
- }
7546
-
7547
- // src/phases/explore.ts
7548
- function explicitlyRejectsTools(error2) {
7549
- if (!error2 || typeof error2 !== "object")
7550
- return false;
7551
- const record = error2;
7552
- const status = Number(record.status ?? record.statusCode ?? record.response?.status);
7553
- const message = String(record.message ?? "");
7554
- return (status === 400 || status === 404 || status === 422) && /(?:(?:tools?|function(?:[ -]calling)?).{0,60}(?:unsupported|not supported|unknown|unavailable|invalid)|(?:unsupported|does not support|doesn't support).{0,60}(?:tools?|function))/i.test(message);
7555
- }
7556
- function shouldExplore(extraction) {
7557
- const unresolvedErrors = extraction.errors.filter((e) => !e.resolved).length;
7558
- const topicCount = extraction.topics.length;
7559
- const decisionCount = extraction.decisions.length;
7560
- const crossFileWork = new Set(extraction.modifiedFiles.map((f) => {
7561
- const parts = f.path.split("/");
7562
- return parts.length > 1 ? parts.slice(0, -1).join("/") : "root";
7563
- })).size;
7564
- if (topicCount <= 3 && unresolvedErrors <= 1 && decisionCount <= 2 && crossFileWork <= 2) {
7565
- return false;
7566
- }
7567
- return true;
7576
+ return true;
7568
7577
  }
7569
7578
  var EXPLORATION_TOOLS = [
7570
7579
  {
@@ -8863,541 +8872,186 @@ async function resolveStageAuth(rc, stage) {
8863
8872
  return resolved;
8864
8873
  }
8865
8874
 
8866
- // src/app/steps/synthesize.ts
8867
- async function summarizeConversation(rc) {
8868
- let synthPhaseStart = Date.now();
8869
- const extraction = rc.extraction;
8870
- rc.mode ??= rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced";
8871
- rc.requestedMode ??= rc.mode;
8872
- if (rc.requestedMode === "auto") {
8873
- const refined = resolveMode("auto", rc.contextPercent, extraction, continuityRisk(rc.previousState) + (rc.adapted ? 12 : 0));
8874
- if (refined !== rc.mode) {
8875
- rc.mode = refined;
8876
- const policy2 = MODE_POLICIES[refined];
8877
- rc.services.budget.setLimits(rc.maxLlmCalls ?? effectiveBudget(rc.config.maxLlmCalls, policy2.maxLlmCalls), rc.maxLlmInputTokens ?? effectiveBudget(rc.config.maxLlmInputTokens, policy2.maxInputTokens), policy2.maxOutputTokens);
8878
- rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
8879
- }
8875
+ // src/phases/verify.ts
8876
+ var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
8877
+ var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
8878
+ var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
8879
+ var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
8880
+ var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
8881
+ function noneBlockerLineIndexes(lines) {
8882
+ const indexes = new Set;
8883
+ const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
8884
+ for (const item of nonEmpty) {
8885
+ if (BULLET_NONE_BLOCKER_RE.test(item.text))
8886
+ indexes.add(item.index);
8880
8887
  }
8881
- const pc = rc.profileCfg;
8882
- const policy = MODE_POLICIES[rc.mode];
8883
- const cacheKey = synthesisCacheKey(rc);
8884
- const cached = getCachedSynthesis(cacheKey);
8885
- if (cached) {
8886
- rc.notify("Synthesis cache hit \u2014 no LLM calls", "info");
8887
- showProgressOverlay(rc.ctx, {
8888
- phase: 3,
8889
- phaseName: "Synthesize",
8890
- detail: "Reusing the cached continuation summary \xB7 no LLM call"
8891
- });
8892
- Object.assign(rc, {
8893
- finalSummary: cached.finalSummary,
8894
- method: cached.method,
8895
- methodForMetrics: cached.method + "-cache",
8896
- generationFallbacks: [],
8897
- llmCalls: 0,
8898
- summaries: cached.summaries,
8899
- explorationReport: cached.explorationReport,
8900
- explorationRounds: cached.explorationRounds,
8901
- chunkCount: cached.chunkCount
8902
- });
8903
- const hit = advance(rc, "_synthesized");
8904
- markMeasuredPhase(hit, "synthesize", synthPhaseStart);
8905
- return hit;
8888
+ if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
8889
+ indexes.add(nonEmpty[0].index);
8906
8890
  }
8907
- const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
8908
- conversationTokens: rc.convTokens,
8909
- toolPercent: rc.toolPercent
8910
- }) >= 0.85;
8911
- if (zeroCall) {
8912
- showProgressOverlay(rc.ctx, {
8913
- phase: 3,
8914
- phaseName: "Synthesize",
8915
- detail: "Building a deterministic continuation summary \xB7 no LLM call"
8916
- });
8917
- const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8918
- setCachedSynthesis(cacheKey, {
8919
- finalSummary: finalSummary2,
8920
- method: "heuristic",
8921
- summaries: [],
8922
- explorationReport: null,
8923
- explorationRounds: 0,
8924
- chunkCount: 0
8925
- });
8926
- rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
8927
- Object.assign(rc, {
8928
- finalSummary: finalSummary2,
8929
- method: "heuristic",
8930
- methodForMetrics: "zero-call",
8931
- generationFallbacks: [],
8932
- llmCalls: 0,
8933
- summaries: [],
8934
- explorationReport: null,
8935
- explorationRounds: 0,
8936
- chunkCount: 0
8937
- });
8938
- const deterministic = advance(rc, "_synthesized");
8939
- markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
8940
- return deterministic;
8891
+ return indexes;
8892
+ }
8893
+ function collectListedPaths(body, expectedPaths) {
8894
+ const values = new Set;
8895
+ const encodedValues = new Set;
8896
+ for (const line of body.split(`
8897
+ `)) {
8898
+ const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
8899
+ if (!raw)
8900
+ continue;
8901
+ if (raw.startsWith('"')) {
8902
+ try {
8903
+ const decoded = JSON.parse(raw);
8904
+ if (typeof decoded === "string") {
8905
+ values.add(decoded);
8906
+ encodedValues.add(decoded);
8907
+ continue;
8908
+ }
8909
+ } catch {}
8910
+ }
8911
+ values.add(raw);
8912
+ if (expectedPaths.has(raw))
8913
+ continue;
8914
+ const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
8915
+ const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
8916
+ if (expectedPaths.has(unchecked))
8917
+ values.add(unchecked);
8941
8918
  }
8942
- const shouldSkipExplore = !policy.explore;
8943
- const convText = rc.convText;
8944
- const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * rc.providerCaps.singlePassTokenMultiplier * policy.singlePassMultiplier);
8945
- rc.vlog("Tier=" + rc.tier + " | convTokens=" + rc.convTokens + " | singlePassMax=" + singlePassMaxTokens);
8946
- let finalSummary;
8947
- let method;
8948
- const summaries = [];
8949
- let explorationReport = null;
8950
- let explorationRounds = 0;
8951
- let chunkCount = 0;
8952
- let cacheable = true;
8953
- const generationFallbacks = [];
8954
- let summaryAuth;
8919
+ return {
8920
+ values,
8921
+ encodedValues,
8922
+ normalizedValues: new Set(Array.from(values, normalizePath))
8923
+ };
8924
+ }
8925
+ function decodePathDisplay(display) {
8955
8926
  try {
8956
- summaryAuth = await resolveStageAuth(rc, "summary");
8957
- } catch (error2) {
8958
- cacheable = false;
8959
- generationFallbacks.push("summary route unavailable");
8960
- debugError("Summary route unavailable", error2);
8961
- rc.notify("Summary route unavailable \xB7 using deterministic fallback", "info");
8927
+ const decoded = JSON.parse(display);
8928
+ return typeof decoded === "string" ? decoded : display;
8929
+ } catch {
8930
+ return display;
8962
8931
  }
8963
- if (!summaryAuth) {
8964
- showProgressOverlay(rc.ctx, {
8965
- phase: 3,
8966
- phaseName: "Synthesize",
8967
- detail: "Summary route unavailable \xB7 building a deterministic summary"
8968
- });
8969
- finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8970
- method = "heuristic";
8971
- } else if (rc.convTokens < singlePassMaxTokens) {
8972
- showProgressOverlay(rc.ctx, {
8973
- phase: 3,
8974
- phaseName: "Synthesize",
8975
- detail: "Writing one continuation summary from " + rc.convTokens.toLocaleString() + " tokens",
8976
- model: rc.modelLabel,
8977
- profile: rc.profile,
8978
- extraction
8979
- });
8980
- try {
8981
- const r = await singlePassCompact(convText, extraction, null, rc.prevContext + rc.projectCtx, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined);
8982
- finalSummary = r.summary;
8983
- method = "single-pass";
8984
- } catch (err) {
8985
- cacheable = false;
8986
- generationFallbacks.push("single-pass generation failed");
8987
- debugError("Single-pass synthesis used deterministic fallback", err);
8988
- rc.notify("Single-pass generation stopped \xB7 using deterministic fallback", "info");
8989
- finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
8990
- method = "heuristic";
8991
- }
8992
- } else {
8993
- const needsExploration = !shouldSkipExplore && shouldExplore(extraction);
8994
- if (needsExploration) {
8995
- const exploreStart = Date.now();
8996
- showProgressOverlay(rc.ctx, {
8997
- phase: 2,
8998
- phaseName: "Explore",
8999
- detail: "Mapping topic shifts and continuity risks",
9000
- model: rc.modelLabel,
9001
- profile: rc.profile,
9002
- extraction
9003
- });
9004
- try {
9005
- const segAuth = await resolveStageAuth(rc, "explore");
9006
- const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [
9007
- rc.userNote,
9008
- rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined
9009
- ].filter(Boolean).join(`
9010
- `) || undefined, rc.cancellation.signal, MAX_EXPLORATION_ROUNDS, rc.notify, rc.services);
9011
- explorationReport = expResult.report;
9012
- explorationRounds = expResult.rounds;
9013
- rc.notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
9014
- rc.vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
9015
- } catch (err) {
9016
- cacheable = false;
9017
- generationFallbacks.push("exploration unavailable");
9018
- debugError("Explore used deterministic topic boundaries", err);
9019
- rc.notify("Explore unavailable \xB7 using deterministic topic boundaries", "info");
9020
- } finally {
9021
- const exploreEnd = Date.now();
9022
- markMeasuredPhase(rc, "explore", exploreStart, exploreEnd);
9023
- synthPhaseStart = exploreEnd;
9024
- }
9025
- } else {
9026
- rc.notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
9027
- }
9028
- let boundaries;
9029
- if (explorationReport?.boundaries.length) {
9030
- const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
9031
- const heuristicBounds = extraction.topics.map((t) => ({
9032
- afterIndex: t.endIndex,
9033
- topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
9034
- priority: t.errorDensity > 2 ? "high" : "normal",
9035
- confidence: 0.6
9036
- }));
9037
- if (llmBounds.length > 0) {
9038
- const merged = [...llmBounds];
9039
- for (const hb of heuristicBounds) {
9040
- const nearby = merged.find((m) => Math.abs(m.afterIndex - hb.afterIndex) <= 3);
9041
- if (!nearby)
9042
- merged.push(hb);
9043
- }
9044
- boundaries = merged.sort((a, b) => a.afterIndex - b.afterIndex);
9045
- } else {
9046
- boundaries = heuristicBounds;
9047
- }
9048
- } else {
9049
- boundaries = extraction.topics.map((t) => ({
9050
- afterIndex: t.endIndex,
9051
- topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
9052
- priority: t.errorDensity > 2 ? "high" : "normal",
9053
- confidence: 0.6
9054
- }));
8932
+ }
8933
+ function hasListedPath(listed, file, display, normalizedOwners) {
8934
+ const decodedDisplay = decodePathDisplay(display);
8935
+ if (listed.encodedValues.has(decodedDisplay))
8936
+ return true;
8937
+ if (PATH_PLACEHOLDER_RE.test(file))
8938
+ return false;
8939
+ if (listed.values.has(file))
8940
+ return true;
8941
+ for (const candidate of [file, decodedDisplay]) {
8942
+ const normalized = normalizePath(candidate);
8943
+ if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
8944
+ return true;
8945
+ }
8946
+ return false;
8947
+ }
8948
+ function outcomeClaims(summary, pathEvidence) {
8949
+ const pathLines = new Set(Array.from(pathEvidence, ([path12, display]) => [path12, display, "`" + path12 + "`"]).flat());
8950
+ return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !pathLines.has(line)).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
8951
+ }
8952
+ function classifyOutcomeClaim(claim) {
8953
+ const lower = claim.toLowerCase();
8954
+ if (/\btests?\b|\btestler?\b/.test(lower))
8955
+ return "test";
8956
+ if (/\bbuild\b|\bcompil(?:e|ed|ation)\b|\btypecheck\b/.test(lower))
8957
+ return "build";
8958
+ if (/\bdeploy(?:ed|ment)?\b|\bpublish(?:ed)?\b|\breleas(?:e|ed)\b/.test(lower))
8959
+ return "release";
8960
+ if (/\bbug\b|\bissue\b|\berror\b|\bfail(?:ed|ure)?\b|\bhata\b/.test(lower))
8961
+ return "error";
8962
+ if (/\bfile\b|\bdosya\b/.test(lower))
8963
+ return "file";
8964
+ return "generic";
8965
+ }
8966
+ var successfulToolEvidenceCache = new WeakMap;
8967
+ var sourceTextCache = new WeakMap;
8968
+ function sourceSupportsFileReference(ref, messages) {
8969
+ let texts = sourceTextCache.get(messages);
8970
+ if (!texts) {
8971
+ texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
8972
+ sourceTextCache.set(messages, texts);
8973
+ }
8974
+ const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
8975
+ if (!needle)
8976
+ return false;
8977
+ for (const text of texts) {
8978
+ let index = text.indexOf(needle);
8979
+ while (index >= 0) {
8980
+ const before = text[index - 1] ?? "";
8981
+ const after = text[index + needle.length] ?? "";
8982
+ if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
8983
+ return true;
8984
+ index = text.indexOf(needle, index + 1);
9055
8985
  }
9056
- const chunks = chunkLlmMessages(rc.llmMessages, boundaries, pc, rc.estimator, rc.config.focusWeighting ? rc.focus : undefined);
9057
- chunkCount = chunks.length;
9058
- rc.notify("Chunked: " + chunkCount + " chunks", "info");
9059
- rc.vlog("Chunk topics: " + chunks.map((c) => c.topic + "[" + c.startIndex + "-" + c.endIndex + "]").join(", "));
9060
- const batches = createBatches(chunks, pc.batchMaxTokens);
9061
- const totalBatches = batches.length;
9062
- showProgressOverlay(rc.ctx, {
9063
- phase: 3,
9064
- phaseName: "Synthesize",
9065
- detail: "Compressing older history \xB7 batch 0/" + totalBatches,
9066
- model: rc.modelLabel,
9067
- profile: rc.profile,
9068
- extraction,
9069
- explorationRounds,
9070
- totalBatches
8986
+ }
8987
+ return false;
8988
+ }
8989
+ function successfulToolEvidence(messages) {
8990
+ const cached = successfulToolEvidenceCache.get(messages);
8991
+ if (cached)
8992
+ return cached;
8993
+ const toolCalls = buildToolCallIndex(messages);
8994
+ const evidence = [];
8995
+ for (const message of messages) {
8996
+ if (message.role !== "toolResult" || message.isError)
8997
+ continue;
8998
+ const call = toolCalls.get(message.toolCallId ?? "");
8999
+ if (!call)
9000
+ continue;
9001
+ const result = extractText(message.content).slice(0, 8000);
9002
+ if (!result.trim() || LIKELY_ERROR_RE.test(result))
9003
+ continue;
9004
+ const command = [call.arguments.command, call.arguments.cmd, call.arguments.script].find((value) => typeof value === "string") ?? "";
9005
+ evidence.push({
9006
+ name: normalizeToolName(call.name),
9007
+ operation: classifyToolOperation(call.arguments, call.name),
9008
+ command,
9009
+ path: extractToolPath(call.arguments),
9010
+ result
9071
9011
  });
9072
- const concurrency = rc.providerCaps.concurrencyLimit;
9073
- if (totalBatches <= 1) {
9074
- const single = batches[0];
9075
- if (single) {
9076
- if (rc.services.budget.remainingCalls() <= 1) {
9077
- summaries.push(...single.map((ch) => failedChunkSummary(ch)));
9078
- cacheable = false;
9079
- generationFallbacks.push("call budget reserved for final assembly");
9080
- rc.notify("Call budget: chunk synthesis uses deterministic evidence so final assembly remains available", "info");
9081
- } else {
9082
- try {
9083
- summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, single.length, rc.providerCaps.maxOutputTokens), rc.sessionId));
9084
- } catch (err) {
9085
- summaries.push(...single.map((ch) => failedChunkSummary(ch)));
9086
- cacheable = false;
9087
- generationFallbacks.push("1 synthesis batch fallback");
9088
- debugError("Synthesis batch used deterministic fallback", err);
9089
- rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage", "info");
9090
- showProgressOverlay(rc.ctx, {
9091
- phase: 3,
9092
- phaseName: "Synthesize",
9093
- detail: "1 batch fallback \xB7 preserving coverage from deterministic evidence",
9094
- explorationRounds
9095
- });
9096
- }
9097
- }
9098
- } else {
9099
- rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
9100
- }
9101
- } else {
9102
- const results = new Array(totalBatches);
9103
- const errors = new Array(totalBatches).fill(null);
9104
- const batchCallLimit = Math.max(0, Math.min(totalBatches, rc.services.budget.remainingCalls() - 1));
9105
- for (let index = batchCallLimit;index < totalBatches; index++) {
9106
- results[index] = batches[index].map((chunk) => failedChunkSummary(chunk));
9107
- }
9108
- if (batchCallLimit < totalBatches) {
9109
- rc.notify("Call budget: " + (totalBatches - batchCallLimit) + " batch(es) use deterministic fallback to reserve assembly", "info");
9110
- cacheable = false;
9111
- generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
9112
- }
9113
- let completed = totalBatches - batchCallLimit;
9114
- let nextBatch = 0;
9115
- let budgetStopped = false;
9116
- const runWorker = async () => {
9117
- while (true) {
9118
- const idx = nextBatch++;
9119
- if (idx >= batchCallLimit)
9120
- return;
9121
- if (budgetStopped || rc.services.budget.reason()) {
9122
- budgetStopped = true;
9123
- results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
9124
- } else {
9125
- try {
9126
- const batch = batches[idx];
9127
- results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
9128
- } catch (err) {
9129
- errors[idx] = err instanceof Error ? err : new Error(String(err));
9130
- results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
9131
- }
9132
- }
9133
- completed++;
9134
- showProgressOverlay(rc.ctx, {
9135
- phase: 3,
9136
- phaseName: "Synthesize",
9137
- detail: "Compressing older history \xB7 batch " + completed + "/" + totalBatches,
9138
- model: rc.modelLabel,
9139
- profile: rc.profile,
9140
- extraction,
9141
- explorationRounds,
9142
- totalBatches,
9143
- currentBatch: completed
9144
- });
9145
- }
9146
- };
9147
- const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
9148
- await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
9149
- if (budgetStopped) {
9150
- rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
9151
- cacheable = false;
9152
- generationFallbacks.push("synthesis budget exhausted during batch pool");
9153
- }
9154
- for (const r of results)
9155
- if (r)
9156
- summaries.push(...r);
9157
- const failedBatches = errors.filter(Boolean);
9158
- for (const error2 of failedBatches)
9159
- debugError("Synthesis batch used deterministic fallback", error2);
9160
- if (failedBatches.length) {
9161
- cacheable = false;
9162
- generationFallbacks.push(failedBatches.length + " synthesis batch fallback(s)");
9163
- rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage", "info");
9164
- showProgressOverlay(rc.ctx, {
9165
- phase: 3,
9166
- phaseName: "Synthesize",
9167
- detail: failedBatches.length + " batch fallback(s) \xB7 preserving coverage from deterministic evidence",
9168
- explorationRounds
9169
- });
9170
- }
9171
- }
9172
- showProgressOverlay(rc.ctx, {
9173
- phase: 3,
9174
- phaseName: "Synthesize",
9175
- detail: "Merging summaries with project continuity",
9176
- model: rc.modelLabel,
9177
- profile: rc.profile,
9178
- extraction,
9179
- explorationRounds,
9180
- totalBatches: batches.length
9181
- });
9182
- try {
9183
- const r = await assembleLLM(summaries, extraction, explorationReport, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.prevContext, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined, rc.previousState);
9184
- if (r?.startsWith("##"))
9185
- finalSummary = r;
9186
- else
9187
- throw new Error("bad");
9188
- } catch (err) {
9189
- cacheable = false;
9190
- generationFallbacks.push("assembly generation failed");
9191
- debugError("Assembly used deterministic fallback", err);
9192
- finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
9193
- }
9194
- method = "eesv";
9195
- }
9196
- Object.assign(rc, {
9197
- finalSummary,
9198
- method,
9199
- methodForMetrics: method,
9200
- generationFallbacks,
9201
- llmCalls: rc.services.metrics.summary().totalCalls,
9202
- summaries,
9203
- explorationReport,
9204
- explorationRounds,
9205
- chunkCount
9206
- });
9207
- const out = advance(rc, "_synthesized");
9208
- if (cacheable) {
9209
- setCachedSynthesis(cacheKey, {
9210
- finalSummary,
9211
- method,
9212
- summaries,
9213
- explorationReport,
9214
- explorationRounds,
9215
- chunkCount
9216
- });
9217
- }
9218
- markMeasuredPhase(out, "synthesize", synthPhaseStart);
9219
- return out;
9220
- }
9221
-
9222
- // src/phases/verify.ts
9223
- var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
9224
- var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
9225
- var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
9226
- var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
9227
- var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
9228
- function noneBlockerLineIndexes(lines) {
9229
- const indexes = new Set;
9230
- const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
9231
- for (const item of nonEmpty) {
9232
- if (BULLET_NONE_BLOCKER_RE.test(item.text))
9233
- indexes.add(item.index);
9234
- }
9235
- if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
9236
- indexes.add(nonEmpty[0].index);
9237
- }
9238
- return indexes;
9239
- }
9240
- function collectListedPaths(body, expectedPaths) {
9241
- const values = new Set;
9242
- const encodedValues = new Set;
9243
- for (const line of body.split(`
9244
- `)) {
9245
- const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
9246
- if (!raw)
9247
- continue;
9248
- if (raw.startsWith('"')) {
9249
- try {
9250
- const decoded = JSON.parse(raw);
9251
- if (typeof decoded === "string") {
9252
- values.add(decoded);
9253
- encodedValues.add(decoded);
9254
- continue;
9255
- }
9256
- } catch {}
9257
- }
9258
- values.add(raw);
9259
- if (expectedPaths.has(raw))
9260
- continue;
9261
- const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
9262
- const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
9263
- if (expectedPaths.has(unchecked))
9264
- values.add(unchecked);
9265
- }
9266
- return {
9267
- values,
9268
- encodedValues,
9269
- normalizedValues: new Set(Array.from(values, normalizePath))
9270
- };
9271
- }
9272
- function decodePathDisplay(display) {
9273
- try {
9274
- const decoded = JSON.parse(display);
9275
- return typeof decoded === "string" ? decoded : display;
9276
- } catch {
9277
- return display;
9278
9012
  }
9013
+ successfulToolEvidenceCache.set(messages, evidence);
9014
+ return evidence;
9279
9015
  }
9280
- function hasListedPath(listed, file, display, normalizedOwners) {
9281
- const decodedDisplay = decodePathDisplay(display);
9282
- if (listed.encodedValues.has(decodedDisplay))
9016
+ function successfulToolSupportsClaim(claim, tools, extraction) {
9017
+ const shape = semanticShape(claim);
9018
+ const category = classifyOutcomeClaim(claim);
9019
+ if (category === "error" && extraction.errors.some((error2) => error2.resolved && hasSemanticEvidence(claim, error2.message)))
9283
9020
  return true;
9284
- if (PATH_PLACEHOLDER_RE.test(file))
9285
- return false;
9286
- if (listed.values.has(file))
9021
+ if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
9287
9022
  return true;
9288
- for (const candidate of [file, decodedDisplay]) {
9289
- const normalized = normalizePath(candidate);
9290
- if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
9023
+ for (const tool of tools) {
9024
+ const operationText = tool.name + " " + tool.command;
9025
+ const operationSupports = category === "test" ? /\b(?:test|tests|pytest|jest|vitest|mocha|rspec)\b/i.test(operationText) : category === "build" ? /\b(?:build|compile|typecheck|tsc|check)\b/i.test(operationText) : category === "release" ? /\b(?:deploy|publish|release)\b/i.test(operationText) : category === "file" ? tool.operation === "mutate" || tool.operation === "delete" : category === "error" ? tool.operation === "execute" || tool.operation === "mutate" || tool.operation === "delete" : tool.operation !== "read" && tool.operation !== "search" && tool.operation !== "list";
9026
+ if (!operationSupports)
9027
+ continue;
9028
+ if (hasSemanticEvidence(claim, tool.result))
9029
+ return true;
9030
+ const lower = tool.result.toLowerCase();
9031
+ if (category === "test" && /\b\d+\s+(?:tests?\s+)?pass(?:ed)?\b/.test(lower) && !/\b(?:fail(?:ed|ures?)?|errors?)\s*[:=]?\s*[1-9]\d*\b/.test(lower))
9032
+ return true;
9033
+ if (category === "build" && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
9034
+ return true;
9035
+ if (category === "release" && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
9036
+ return true;
9037
+ if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, tool.result))
9291
9038
  return true;
9292
9039
  }
9293
9040
  return false;
9294
9041
  }
9295
- function outcomeClaims(summary) {
9296
- return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
9297
- }
9298
- function classifyOutcomeClaim(claim) {
9299
- const lower = claim.toLowerCase();
9300
- if (/\btests?\b|\btestler?\b/.test(lower))
9301
- return "test";
9302
- if (/\bbuild\b|\bcompil(?:e|ed|ation)\b|\btypecheck\b/.test(lower))
9303
- return "build";
9304
- if (/\bdeploy(?:ed|ment)?\b|\bpublish(?:ed)?\b|\breleas(?:e|ed)\b/.test(lower))
9305
- return "release";
9306
- if (/\bbug\b|\bissue\b|\berror\b|\bfail(?:ed|ure)?\b|\bhata\b/.test(lower))
9307
- return "error";
9308
- if (/\bfile\b|\bdosya\b/.test(lower))
9309
- return "file";
9310
- return "generic";
9311
- }
9312
- var successfulToolEvidenceCache = new WeakMap;
9313
- var sourceTextCache = new WeakMap;
9314
- function sourceSupportsFileReference(ref, messages) {
9315
- let texts = sourceTextCache.get(messages);
9316
- if (!texts) {
9317
- texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
9318
- sourceTextCache.set(messages, texts);
9319
- }
9320
- const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
9321
- if (!needle)
9322
- return false;
9323
- for (const text of texts) {
9324
- let index = text.indexOf(needle);
9325
- while (index >= 0) {
9326
- const before = text[index - 1] ?? "";
9327
- const after = text[index + needle.length] ?? "";
9328
- if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
9329
- return true;
9330
- index = text.indexOf(needle, index + 1);
9331
- }
9332
- }
9333
- return false;
9334
- }
9335
- function successfulToolEvidence(messages) {
9336
- const cached = successfulToolEvidenceCache.get(messages);
9337
- if (cached)
9338
- return cached;
9339
- const toolCalls = buildToolCallIndex(messages);
9340
- const evidence = [];
9341
- for (const message of messages) {
9342
- if (message.role !== "toolResult" || message.isError)
9343
- continue;
9344
- const call = toolCalls.get(message.toolCallId ?? "");
9345
- if (!call)
9346
- continue;
9347
- const result = extractText(message.content).slice(0, 8000);
9348
- if (!result.trim() || LIKELY_ERROR_RE.test(result))
9349
- continue;
9350
- const command = [call.arguments.command, call.arguments.cmd, call.arguments.script].find((value) => typeof value === "string") ?? "";
9351
- evidence.push({
9352
- name: normalizeToolName(call.name),
9353
- operation: classifyToolOperation(call.arguments, call.name),
9354
- command,
9355
- path: extractToolPath(call.arguments),
9356
- result
9357
- });
9358
- }
9359
- successfulToolEvidenceCache.set(messages, evidence);
9360
- return evidence;
9361
- }
9362
- function successfulToolSupportsClaim(claim, tools, extraction) {
9363
- const shape = semanticShape(claim);
9364
- const category = classifyOutcomeClaim(claim);
9365
- if (category === "error" && extraction.errors.some((error2) => error2.resolved && hasSemanticEvidence(claim, error2.message)))
9366
- return true;
9367
- if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
9368
- return true;
9369
- for (const tool of tools) {
9370
- const operationText = tool.name + " " + tool.command;
9371
- const operationSupports = category === "test" ? /\b(?:test|tests|pytest|jest|vitest|mocha|rspec)\b/i.test(operationText) : category === "build" ? /\b(?:build|compile|typecheck|tsc|check)\b/i.test(operationText) : category === "release" ? /\b(?:deploy|publish|release)\b/i.test(operationText) : category === "file" ? tool.operation === "mutate" || tool.operation === "delete" : category === "error" ? tool.operation === "execute" || tool.operation === "mutate" || tool.operation === "delete" : tool.operation !== "read" && tool.operation !== "search" && tool.operation !== "list";
9372
- if (!operationSupports)
9373
- continue;
9374
- if (hasSemanticEvidence(claim, tool.result))
9375
- return true;
9376
- const lower = tool.result.toLowerCase();
9377
- if (category === "test" && /\b\d+\s+(?:tests?\s+)?pass(?:ed)?\b/.test(lower) && !/\b(?:fail(?:ed|ures?)?|errors?)\s*[:=]?\s*[1-9]\d*\b/.test(lower))
9378
- return true;
9379
- if (category === "build" && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
9380
- return true;
9381
- if (category === "release" && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
9382
- return true;
9383
- if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, tool.result))
9384
- return true;
9385
- }
9386
- return false;
9387
- }
9388
- function removeUnsupportedClaim(summary, claim) {
9389
- const normalized = claim.replace(/\s+/g, " ").trim().toLocaleLowerCase();
9390
- return {
9391
- sections: summary.sections.map((section) => ({
9392
- ...section,
9393
- body: section.body.split(`
9394
- `).filter((line) => {
9395
- const candidate = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").replace(/\s+/g, " ").trim().toLocaleLowerCase();
9396
- return candidate !== normalized;
9397
- }).join(`
9398
- `).trim()
9399
- }))
9400
- };
9042
+ function removeUnsupportedClaim(summary, claim) {
9043
+ const normalized = claim.replace(/\s+/g, " ").trim().toLocaleLowerCase();
9044
+ return {
9045
+ sections: summary.sections.map((section) => ({
9046
+ ...section,
9047
+ body: section.body.split(`
9048
+ `).filter((line) => {
9049
+ const candidate = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").replace(/\s+/g, " ").trim().toLocaleLowerCase();
9050
+ return candidate !== normalized;
9051
+ }).join(`
9052
+ `).trim()
9053
+ }))
9054
+ };
9401
9055
  }
9402
9056
  function formatVerificationGap(gap) {
9403
9057
  switch (gap.kind) {
@@ -9592,7 +9246,7 @@ function stemToken(token) {
9592
9246
  return lower;
9593
9247
  }
9594
9248
  function semanticTokens(text) {
9595
- return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
9249
+ return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2 || NEGATION_MARKERS.has(token));
9596
9250
  }
9597
9251
  var semanticShapeCache = new Map;
9598
9252
  var semanticFragmentCache = new Map;
@@ -9612,7 +9266,7 @@ function hasEffectiveTargetNegation(tokens, anchor) {
9612
9266
  if (token !== anchor)
9613
9267
  return false;
9614
9268
  const nearbyStart = Math.max(0, anchorIndex - 2);
9615
- const nearbyNegations = tokens.slice(nearbyStart, anchorIndex + 3).map((near, offset) => NEGATION_MARKERS.has(near) ? nearbyStart + offset : -1).filter((index) => index >= 0);
9269
+ const nearbyNegations = tokens.slice(nearbyStart, anchorIndex + 3).map((near, offset) => NEGATION_MARKERS.has(near) && !(near === "without" && nearbyStart + offset > anchorIndex) ? nearbyStart + offset : -1).filter((index) => index >= 0);
9616
9270
  const governingStart = Math.max(0, anchorIndex - 3);
9617
9271
  const preceding = tokens.slice(governingStart, anchorIndex);
9618
9272
  const nearbyGuards = preceding.map((near, offset) => POLARITY_INVERTING_GUARDS.has(near) ? governingStart + offset : -1).filter((index) => index >= 0);
@@ -9661,12 +9315,13 @@ function hasSemanticEvidence(source, target) {
9661
9315
  });
9662
9316
  }
9663
9317
  function hasSemanticContradiction(source, target) {
9318
+ const sourceFragments = new Set(semanticFragments(source).map((tokens) => tokens.join(" ")));
9664
9319
  const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
9665
9320
  if (!anchor)
9666
9321
  return false;
9667
9322
  const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
9668
9323
  return semanticFragments(target).some((tokens) => {
9669
- if (!tokens.includes(anchor))
9324
+ if (!tokens.includes(anchor) || sourceFragments.has(tokens.join(" ")))
9670
9325
  return false;
9671
9326
  const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
9672
9327
  if (overlap < required)
@@ -9951,7 +9606,7 @@ function verifyProgressConsistency(parsed, extraction, collected, paths, accumul
9951
9606
  }
9952
9607
  }
9953
9608
  }
9954
- function verifyOpenLoopsAndClaims(summary, parsed, extraction, continuity, evidence, collected, accumulator) {
9609
+ function verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity, evidence, collected, accumulator) {
9955
9610
  const unresolvedCount = collected.unresolved.length + (continuity?.openLoops.filter((loop) => loop.status !== "resolved").length ?? 0);
9956
9611
  if (unresolvedCount >= 1 && !findSection(parsed, "open-loops") && !summary.toLowerCase().replace(/\\/g, "/").includes("unresolved")) {
9957
9612
  addGap(accumulator, { kind: "missing-open-loops", unresolvedCount }, 5);
@@ -9959,7 +9614,7 @@ function verifyOpenLoopsAndClaims(summary, parsed, extraction, continuity, evide
9959
9614
  if (!evidence.sourceMessages)
9960
9615
  return;
9961
9616
  const tools = successfulToolEvidence(evidence.sourceMessages);
9962
- for (const claim of outcomeClaims(summary)) {
9617
+ for (const claim of outcomeClaims(summary, paths.rendered)) {
9963
9618
  if (!successfulToolSupportsClaim(claim, tools, extraction)) {
9964
9619
  addGap(accumulator, { kind: "unsupported-claim", claim }, 20);
9965
9620
  }
@@ -9975,7 +9630,7 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
9975
9630
  verifySemanticCoverage(parsed, collected, accumulator);
9976
9631
  verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator);
9977
9632
  verifyProgressConsistency(parsed, extraction, collected, paths, accumulator);
9978
- verifyOpenLoopsAndClaims(summary, parsed, extraction, continuity, evidence, collected, accumulator);
9633
+ verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity, evidence, collected, accumulator);
9979
9634
  const score = Math.max(0, accumulator.score);
9980
9635
  return {
9981
9636
  ok: accumulator.gaps.length === 0 && score >= 85,
@@ -10065,118 +9720,563 @@ function patchDeterministic(summary, gaps, extraction, continuity = null, eviden
10065
9720
  if (!existing.includes(message.toLowerCase())) {
10066
9721
  canonical = appendToSection(canonical, "critical-context", "- " + (gap.resolved ? "Resolved error: " : "Unresolved error: ") + message);
10067
9722
  }
10068
- break;
10069
- }
10070
- case "missing-constraint":
10071
- canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
10072
- break;
10073
- case "missing-decision":
10074
- canonical = appendToSection(canonical, "decisions", "- **" + safe(gap.summary, TRUNC.DECISION_SUMMARY) + "**");
10075
- break;
10076
- case "missing-goal":
10077
- canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
10078
- break;
10079
- case "missing-open-loops": {
10080
- const current = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
10081
- const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
10082
- const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({
10083
- priority: loop.priority,
10084
- summary: safe(loop.summary, TRUNC.SNIPPET)
10085
- })).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
10086
- const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
10087
- `);
10088
- canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
10089
- break;
9723
+ break;
9724
+ }
9725
+ case "missing-constraint":
9726
+ canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
9727
+ break;
9728
+ case "missing-decision":
9729
+ canonical = appendToSection(canonical, "decisions", "- **" + safe(gap.summary, TRUNC.DECISION_SUMMARY) + "**");
9730
+ break;
9731
+ case "missing-goal":
9732
+ canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
9733
+ break;
9734
+ case "missing-open-loops": {
9735
+ const current = extraction.errors.filter((error2) => !error2.resolved).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
9736
+ const carriedErrors = (continuity?.unresolvedErrors ?? []).map((error2) => safe(error2.message, TRUNC.SNIPPET)).filter(Boolean).map((message) => "- [high] Resolve " + message);
9737
+ const carriedLoops = (continuity?.openLoops ?? []).filter((loop) => loop.status !== "resolved").map((loop) => ({
9738
+ priority: loop.priority,
9739
+ summary: safe(loop.summary, TRUNC.SNIPPET)
9740
+ })).filter((item) => item.summary).map((item) => "- [" + item.priority + "] " + item.summary);
9741
+ const body = Array.from(new Set([...current, ...carriedErrors, ...carriedLoops])).slice(0, gap.unresolvedCount).join(`
9742
+ `);
9743
+ canonical = upsertSection(canonical, "open-loops", body || "- Review unresolved errors.", "next-steps");
9744
+ break;
9745
+ }
9746
+ case "fabricated-file": {
9747
+ const normalizedRef = gap.ref.replace(/\\/g, "/").toLowerCase();
9748
+ canonical = {
9749
+ sections: canonical.sections.map((section) => ({
9750
+ ...section,
9751
+ body: section.body.split(`
9752
+ `).filter((line) => {
9753
+ if (!/^\s*[-*]\s+/.test(line))
9754
+ return true;
9755
+ const matches = extractFileRefs(line).some((ref) => ref.replace(/\\/g, "/").toLowerCase() === normalizedRef);
9756
+ return !matches;
9757
+ }).join(`
9758
+ `).trim()
9759
+ }))
9760
+ };
9761
+ break;
9762
+ }
9763
+ case "unsupported-claim":
9764
+ canonical = removeUnsupportedClaim(canonical, gap.claim);
9765
+ break;
9766
+ case "inconsistency":
9767
+ if (gap.detail.startsWith("blocked-none:"))
9768
+ patchBlockedNone();
9769
+ break;
9770
+ }
9771
+ }
9772
+ return renderSummary(canonical, { canonicalHeadings: true });
9773
+ }
9774
+ function hasUnclosedMarkdownFence(markdown) {
9775
+ let open = null;
9776
+ for (const line of markdown.split(/\r?\n/)) {
9777
+ const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
9778
+ if (!match)
9779
+ continue;
9780
+ const marker = match[1][0];
9781
+ if (!open) {
9782
+ open = { marker, length: match[1].length };
9783
+ } else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
9784
+ open = null;
9785
+ }
9786
+ }
9787
+ return open !== null;
9788
+ }
9789
+ function patchResponseIsTruncated(patched, stopReason) {
9790
+ const reason = String(stopReason ?? "");
9791
+ return /(?:length|truncat|max(?:imum)?[_ -]?(?:output[_ -]?)?tokens?|token[_ -]?limit)/i.test(reason) || /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
9792
+ }
9793
+ function sectionIdentity(section) {
9794
+ return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
9795
+ }
9796
+ async function patchSummary(summary, gaps, model, auth, signal, services) {
9797
+ const patchPrompt = `Correct every verification finding below WITHOUT restructuring the summary. Add missing evidence, remove fabricated references, and rewrite contradictory claims so they preserve the source constraint/decision polarity. Do not add a Verification Note.
9798
+
9799
+ Findings:
9800
+ ` + gaps.map((gap, index) => index + 1 + ". " + formatVerificationGap(gap)).join(`
9801
+ `) + `
9802
+
9803
+ Current summary:
9804
+ ` + summary + `
9805
+
9806
+ Return the COMPLETE corrected summary in the same format.`;
9807
+ try {
9808
+ const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
9809
+ const response = await trackedComplete("patch", model, {
9810
+ systemPrompt: COMPACT_SYSTEM_PREFIX,
9811
+ messages: [
9812
+ {
9813
+ role: "user",
9814
+ content: [{ type: "text", text: patchPrompt }],
9815
+ timestamp: Date.now()
9816
+ }
9817
+ ]
9818
+ }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
9819
+ const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
9820
+ `).trim();
9821
+ if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
9822
+ return summary;
9823
+ const originalSections = parseSummary(summary).sections;
9824
+ const patchedSections = parseSummary(patched).sections;
9825
+ const patchedBodies = new Map(patchedSections.map((section) => [
9826
+ sectionIdentity(section),
9827
+ section.body.trim()
9828
+ ]));
9829
+ const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
9830
+ return preserved ? patched : summary;
9831
+ } catch (error2) {
9832
+ debug("patchSummary LLM failed", error2);
9833
+ return summary;
9834
+ }
9835
+ }
9836
+
9837
+ // src/domain/yield-gate.ts
9838
+ class YieldGateError extends Error {
9839
+ reason;
9840
+ name = "YieldGateError";
9841
+ constructor(reason, estimate) {
9842
+ super(reason === "target-miss" ? "Final summary estimate misses the planned compaction target" : "Final summary estimate does not meet the minimum saving policy");
9843
+ this.reason = reason;
9844
+ Object.assign(this, estimate);
9845
+ }
9846
+ plannedAfterTokens;
9847
+ plannedSavedTokens;
9848
+ plannedYield;
9849
+ estimatedAfterTokens;
9850
+ estimatedSavedTokens;
9851
+ estimatedYield;
9852
+ retainedTailTokens;
9853
+ summaryTokens;
9854
+ summaryBudgetTokens;
9855
+ targetAfterTokens;
9856
+ relaxedSoftBoundaries;
9857
+ hardBoundaryAdjusted;
9858
+ }
9859
+ function verifyCompactionYield(totalTokens, summaryTokens, plan) {
9860
+ const estimatedAfterTokens = plan.fixedContextTokens + plan.retainedTokens + summaryTokens;
9861
+ const estimatedSavedTokens = Math.max(0, totalTokens - estimatedAfterTokens);
9862
+ const estimatedYield = totalTokens > 0 ? estimatedSavedTokens / totalTokens : 0;
9863
+ const estimate = {
9864
+ plannedAfterTokens: plan.projectedAfterTokens,
9865
+ plannedSavedTokens: plan.projectedSavedTokens,
9866
+ plannedYield: plan.projectedYield,
9867
+ estimatedAfterTokens,
9868
+ estimatedSavedTokens,
9869
+ estimatedYield,
9870
+ retainedTailTokens: plan.retainedTokens,
9871
+ summaryTokens,
9872
+ summaryBudgetTokens: plan.summaryBudgetTokens,
9873
+ targetAfterTokens: plan.targetAfterTokens,
9874
+ relaxedSoftBoundaries: plan.relaxedSoftBoundaries,
9875
+ hardBoundaryAdjusted: plan.hardBoundaryAdjusted
9876
+ };
9877
+ if (estimatedAfterTokens > plan.targetAfterTokens + ESTIMATOR_ROUNDING_TOLERANCE_TOKENS) {
9878
+ throw new YieldGateError("target-miss", estimate);
9879
+ }
9880
+ if (estimatedYield < MIN_COMPACTION_SAVING_RATIO) {
9881
+ throw new YieldGateError("insufficient-saving", estimate);
9882
+ }
9883
+ return estimate;
9884
+ }
9885
+
9886
+ // src/ui/error-format.ts
9887
+ function failureAction(kind) {
9888
+ switch (kind) {
9889
+ case "authentication":
9890
+ return "Check the selected model's credentials or use /login.";
9891
+ case "rate-limit":
9892
+ return "Wait for the provider quota to recover before retrying.";
9893
+ case "timeout":
9894
+ return "Try Fast or review the latency budget in /smart-compact settings.";
9895
+ case "budget":
9896
+ return "Review call/token limits in /smart-compact settings.";
9897
+ case "output-limit":
9898
+ return "Review the model output limit and reasoning level.";
9899
+ case "provider":
9900
+ case "validation":
9901
+ return "Check model availability and /smart-compact metrics; select another route manually if needed.";
9902
+ case "cancelled":
9903
+ return "Retry when ready.";
9904
+ default:
9905
+ return "For local stack diagnostics, restart Pi with DEBUG=smart-compact and reproduce.";
9906
+ }
9907
+ }
9908
+ function formatGenerationFailureForUi(error2) {
9909
+ const kind = classifyTelemetryFailure(error2);
9910
+ return kind + ". " + failureAction(kind);
9911
+ }
9912
+ function formatCompactErrorForUi(error2) {
9913
+ if (error2 instanceof VerificationGateError) {
9914
+ const kinds = error2.gapKinds.slice(0, 4).join(", ") || "unknown";
9915
+ return "Verification stopped apply at the " + error2.stage + " gate: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. Conversation unchanged. " + "Review /smart-compact metrics; do not bypass verification. For local evidence, restart Pi with DEBUG=smart-compact.";
9916
+ }
9917
+ if (error2 instanceof YieldGateError) {
9918
+ const reason = error2.reason === "target-miss" ? "target missed" : "saving below 10%";
9919
+ return "Yield check stopped apply: estimated " + error2.estimatedAfterTokens.toLocaleString() + "t after vs " + error2.targetAfterTokens.toLocaleString() + "t target (" + reason + "). Conversation unchanged. Try /smart-compact balanced for a larger target; safety checks still apply.";
9920
+ }
9921
+ return "Smart compact failed [" + classifyTelemetryFailure(error2) + "]. Conversation unchanged. " + failureAction(classifyTelemetryFailure(error2));
9922
+ }
9923
+
9924
+ // src/app/steps/synthesize.ts
9925
+ async function summarizeConversation(rc) {
9926
+ let synthPhaseStart = Date.now();
9927
+ const extraction = rc.extraction;
9928
+ rc.mode ??= rc.profile ? modeFromLegacyProfile(rc.profile) : "balanced";
9929
+ rc.requestedMode ??= rc.mode;
9930
+ if (rc.requestedMode === "auto") {
9931
+ const refined = resolveMode("auto", rc.contextPercent, extraction, continuityRisk(rc.previousState) + (rc.adapted ? 12 : 0));
9932
+ if (refined !== rc.mode) {
9933
+ rc.mode = refined;
9934
+ const policy2 = MODE_POLICIES[refined];
9935
+ rc.services.budget.setLimits(resolveCallBudget(rc.config.maxLlmCalls, refined, rc.maxLlmCalls, rc.flags.autoTriggered && !rc.flags.skipCompact), effectiveBudget(rc.config.maxLlmInputTokens, policy2.maxInputTokens, rc.maxLlmInputTokens), policy2.maxOutputTokens);
9936
+ rc.notify("Auto strategy refined to " + refined + " within the planned " + rc.profile + " window", "info");
9937
+ }
9938
+ }
9939
+ const pc = rc.profileCfg;
9940
+ const policy = MODE_POLICIES[rc.mode];
9941
+ const cacheKey = synthesisCacheKey(rc);
9942
+ const cached = getCachedSynthesis(cacheKey);
9943
+ if (cached) {
9944
+ rc.notify("Synthesis cache hit \u2014 no LLM calls", "info");
9945
+ showProgressOverlay(rc.ctx, {
9946
+ phase: 3,
9947
+ phaseName: "Synthesize",
9948
+ detail: "Reusing the cached continuation summary \xB7 no LLM call"
9949
+ });
9950
+ Object.assign(rc, {
9951
+ finalSummary: cached.finalSummary,
9952
+ method: cached.method,
9953
+ methodForMetrics: cached.method + "-cache",
9954
+ generationFallbacks: [],
9955
+ llmCalls: 0,
9956
+ summaries: cached.summaries,
9957
+ explorationReport: cached.explorationReport,
9958
+ explorationRounds: cached.explorationRounds,
9959
+ chunkCount: cached.chunkCount
9960
+ });
9961
+ const hit = advance(rc, "_synthesized");
9962
+ markMeasuredPhase(hit, "synthesize", synthPhaseStart);
9963
+ return hit;
9964
+ }
9965
+ const zeroCall = rc.config.zeroCallEnabled !== false && rc.mode === "fast" && !rc.focus && !rc.userNote && deterministicExtractionConfidence(extraction, {
9966
+ conversationTokens: rc.convTokens,
9967
+ toolPercent: rc.toolPercent
9968
+ }) >= 0.85;
9969
+ if (zeroCall) {
9970
+ showProgressOverlay(rc.ctx, {
9971
+ phase: 3,
9972
+ phaseName: "Synthesize",
9973
+ detail: "Building a deterministic continuation summary \xB7 no LLM call"
9974
+ });
9975
+ const finalSummary2 = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
9976
+ setCachedSynthesis(cacheKey, {
9977
+ finalSummary: finalSummary2,
9978
+ method: "heuristic",
9979
+ summaries: [],
9980
+ explorationReport: null,
9981
+ explorationRounds: 0,
9982
+ chunkCount: 0
9983
+ });
9984
+ rc.notify("Zero-call deterministic compaction (high-confidence extraction)", "info");
9985
+ Object.assign(rc, {
9986
+ finalSummary: finalSummary2,
9987
+ method: "heuristic",
9988
+ methodForMetrics: "zero-call",
9989
+ generationFallbacks: [],
9990
+ llmCalls: 0,
9991
+ summaries: [],
9992
+ explorationReport: null,
9993
+ explorationRounds: 0,
9994
+ chunkCount: 0
9995
+ });
9996
+ const deterministic = advance(rc, "_synthesized");
9997
+ markMeasuredPhase(deterministic, "synthesize", synthPhaseStart);
9998
+ return deterministic;
9999
+ }
10000
+ const shouldSkipExplore = !policy.explore;
10001
+ const convText = rc.convText;
10002
+ const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * rc.providerCaps.singlePassTokenMultiplier * policy.singlePassMultiplier);
10003
+ rc.vlog("Tier=" + rc.tier + " | convTokens=" + rc.convTokens + " | singlePassMax=" + singlePassMaxTokens);
10004
+ let finalSummary;
10005
+ let method;
10006
+ const summaries = [];
10007
+ let explorationReport = null;
10008
+ let explorationRounds = 0;
10009
+ let chunkCount = 0;
10010
+ let cacheable = true;
10011
+ const generationFallbacks = [];
10012
+ let summaryAuth;
10013
+ try {
10014
+ summaryAuth = await resolveStageAuth(rc, "summary");
10015
+ } catch (error2) {
10016
+ cacheable = false;
10017
+ generationFallbacks.push("summary route unavailable");
10018
+ debugError("Summary route unavailable", error2);
10019
+ rc.notify("Summary route unavailable \xB7 using deterministic fallback [" + formatGenerationFailureForUi(error2) + "]", "warning");
10020
+ }
10021
+ if (!summaryAuth) {
10022
+ showProgressOverlay(rc.ctx, {
10023
+ phase: 3,
10024
+ phaseName: "Synthesize",
10025
+ detail: "Summary route unavailable \xB7 building a deterministic summary"
10026
+ });
10027
+ finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
10028
+ method = "heuristic";
10029
+ } else if (rc.convTokens < singlePassMaxTokens) {
10030
+ showProgressOverlay(rc.ctx, {
10031
+ phase: 3,
10032
+ phaseName: "Synthesize",
10033
+ detail: "Writing one continuation summary from " + rc.convTokens.toLocaleString() + " tokens",
10034
+ model: rc.modelLabel,
10035
+ profile: rc.profile,
10036
+ extraction
10037
+ });
10038
+ try {
10039
+ const r = await singlePassCompact(convText, extraction, null, rc.prevContext + rc.projectCtx, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined);
10040
+ finalSummary = r.summary;
10041
+ method = "single-pass";
10042
+ } catch (err) {
10043
+ cacheable = false;
10044
+ generationFallbacks.push("single-pass generation failed");
10045
+ debugError("Single-pass synthesis used deterministic fallback", err);
10046
+ rc.notify("Single-pass generation stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "warning");
10047
+ finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
10048
+ method = "heuristic";
10049
+ }
10050
+ } else {
10051
+ const needsExploration = !shouldSkipExplore && shouldExplore(extraction);
10052
+ if (needsExploration) {
10053
+ const exploreStart = Date.now();
10054
+ showProgressOverlay(rc.ctx, {
10055
+ phase: 2,
10056
+ phaseName: "Explore",
10057
+ detail: "Mapping topic shifts and continuity risks",
10058
+ model: rc.modelLabel,
10059
+ profile: rc.profile,
10060
+ extraction
10061
+ });
10062
+ try {
10063
+ const segAuth = await resolveStageAuth(rc, "explore");
10064
+ const expResult = await exploreConversation(rc.llmMessages, extraction, rc.segModel, segAuth, rc.prevContext || undefined, [
10065
+ rc.userNote,
10066
+ rc.config.focusWeighting && rc.focus ? "Focus extra preservation on: " + rc.focus : undefined
10067
+ ].filter(Boolean).join(`
10068
+ `) || undefined, rc.cancellation.signal, MAX_EXPLORATION_ROUNDS, rc.notify, rc.services);
10069
+ explorationReport = expResult.report;
10070
+ explorationRounds = expResult.rounds;
10071
+ rc.notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
10072
+ rc.vlog("Explore boundaries: " + explorationReport.boundaries.map((b) => b.afterIndex + "(" + b.confidence.toFixed(2) + ")").join(", "));
10073
+ } catch (err) {
10074
+ cacheable = false;
10075
+ generationFallbacks.push("exploration unavailable");
10076
+ debugError("Explore used deterministic topic boundaries", err);
10077
+ rc.notify("Explore unavailable \xB7 using deterministic topic boundaries", "info");
10078
+ } finally {
10079
+ const exploreEnd = Date.now();
10080
+ markMeasuredPhase(rc, "explore", exploreStart, exploreEnd);
10081
+ synthPhaseStart = exploreEnd;
10082
+ }
10083
+ } else {
10084
+ rc.notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
10085
+ }
10086
+ let boundaries;
10087
+ if (explorationReport?.boundaries.length) {
10088
+ const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
10089
+ const heuristicBounds = extraction.topics.map((t) => ({
10090
+ afterIndex: t.endIndex,
10091
+ topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
10092
+ priority: t.errorDensity > 2 ? "high" : "normal",
10093
+ confidence: 0.6
10094
+ }));
10095
+ if (llmBounds.length > 0) {
10096
+ const merged = [...llmBounds];
10097
+ for (const hb of heuristicBounds) {
10098
+ const nearby = merged.find((m) => Math.abs(m.afterIndex - hb.afterIndex) <= 3);
10099
+ if (!nearby)
10100
+ merged.push(hb);
10101
+ }
10102
+ boundaries = merged.sort((a, b) => a.afterIndex - b.afterIndex);
10103
+ } else {
10104
+ boundaries = heuristicBounds;
10105
+ }
10106
+ } else {
10107
+ boundaries = extraction.topics.map((t) => ({
10108
+ afterIndex: t.endIndex,
10109
+ topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
10110
+ priority: t.errorDensity > 2 ? "high" : "normal",
10111
+ confidence: 0.6
10112
+ }));
10113
+ }
10114
+ const chunks = chunkLlmMessages(rc.llmMessages, boundaries, pc, rc.estimator, rc.config.focusWeighting ? rc.focus : undefined);
10115
+ chunkCount = chunks.length;
10116
+ rc.notify("Chunked: " + chunkCount + " chunks", "info");
10117
+ rc.vlog("Chunk topics: " + chunks.map((c) => c.topic + "[" + c.startIndex + "-" + c.endIndex + "]").join(", "));
10118
+ const batches = createBatches(chunks, pc.batchMaxTokens);
10119
+ const totalBatches = batches.length;
10120
+ showProgressOverlay(rc.ctx, {
10121
+ phase: 3,
10122
+ phaseName: "Synthesize",
10123
+ detail: "Compressing older history \xB7 batch 0/" + totalBatches,
10124
+ model: rc.modelLabel,
10125
+ profile: rc.profile,
10126
+ extraction,
10127
+ explorationRounds,
10128
+ totalBatches
10129
+ });
10130
+ const concurrency = rc.providerCaps.concurrencyLimit;
10131
+ if (totalBatches <= 1) {
10132
+ const single = batches[0];
10133
+ if (single) {
10134
+ if (rc.services.budget.remainingCalls() <= 1) {
10135
+ summaries.push(...single.map((ch) => failedChunkSummary(ch)));
10136
+ cacheable = false;
10137
+ generationFallbacks.push("call budget reserved for final assembly");
10138
+ rc.notify("Call budget: chunk synthesis uses deterministic evidence so final assembly remains available", "info");
10139
+ } else {
10140
+ try {
10141
+ summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, single.length, rc.providerCaps.maxOutputTokens), rc.sessionId));
10142
+ } catch (err) {
10143
+ summaries.push(...single.map((ch) => failedChunkSummary(ch)));
10144
+ cacheable = false;
10145
+ generationFallbacks.push("1 synthesis batch fallback");
10146
+ debugError("Synthesis batch used deterministic fallback", err);
10147
+ rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(err) + "]", "warning");
10148
+ showProgressOverlay(rc.ctx, {
10149
+ phase: 3,
10150
+ phaseName: "Synthesize",
10151
+ detail: "1 batch fallback \xB7 preserving coverage from deterministic evidence",
10152
+ explorationRounds
10153
+ });
10154
+ }
10155
+ }
10156
+ } else {
10157
+ rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
10158
+ }
10159
+ } else {
10160
+ const results = new Array(totalBatches);
10161
+ const errors = new Array(totalBatches).fill(null);
10162
+ const batchCallLimit = Math.max(0, Math.min(totalBatches, rc.services.budget.remainingCalls() - 1));
10163
+ for (let index = batchCallLimit;index < totalBatches; index++) {
10164
+ results[index] = batches[index].map((chunk) => failedChunkSummary(chunk));
10165
+ }
10166
+ if (batchCallLimit < totalBatches) {
10167
+ rc.notify("Call budget: " + (totalBatches - batchCallLimit) + " batch(es) use deterministic fallback to reserve assembly", "info");
10168
+ cacheable = false;
10169
+ generationFallbacks.push(totalBatches - batchCallLimit + " synthesis batch budget fallback(s)");
10170
+ }
10171
+ let completed = totalBatches - batchCallLimit;
10172
+ let nextBatch = 0;
10173
+ let budgetStopped = false;
10174
+ const runWorker = async () => {
10175
+ while (true) {
10176
+ const idx = nextBatch++;
10177
+ if (idx >= batchCallLimit)
10178
+ return;
10179
+ if (budgetStopped || rc.services.budget.reason()) {
10180
+ budgetStopped = true;
10181
+ results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
10182
+ } else {
10183
+ try {
10184
+ const batch = batches[idx];
10185
+ results[idx] = await summarizeBatch(batch, extraction, rc.summaryModel, summaryAuth, rc.cancellation.signal, rc.services, batchOutputLimit(rc.mode, batch.length, rc.providerCaps.maxOutputTokens), rc.sessionId);
10186
+ } catch (err) {
10187
+ errors[idx] = err instanceof Error ? err : new Error(String(err));
10188
+ results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
10189
+ }
10190
+ }
10191
+ completed++;
10192
+ showProgressOverlay(rc.ctx, {
10193
+ phase: 3,
10194
+ phaseName: "Synthesize",
10195
+ detail: "Compressing older history \xB7 batch " + completed + "/" + totalBatches,
10196
+ model: rc.modelLabel,
10197
+ profile: rc.profile,
10198
+ extraction,
10199
+ explorationRounds,
10200
+ totalBatches,
10201
+ currentBatch: completed
10202
+ });
10203
+ }
10204
+ };
10205
+ const workerCount = Math.max(1, Math.min(concurrency, batchCallLimit));
10206
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
10207
+ if (budgetStopped) {
10208
+ rc.notify("Synthesis budget reached \xB7 remaining batches use deterministic fallback", "info");
10209
+ cacheable = false;
10210
+ generationFallbacks.push("synthesis budget exhausted during batch pool");
10090
10211
  }
10091
- case "fabricated-file": {
10092
- const normalizedRef = gap.ref.replace(/\\/g, "/").toLowerCase();
10093
- canonical = {
10094
- sections: canonical.sections.map((section) => ({
10095
- ...section,
10096
- body: section.body.split(`
10097
- `).filter((line) => {
10098
- if (!/^\s*[-*]\s+/.test(line))
10099
- return true;
10100
- const matches = extractFileRefs(line).some((ref) => ref.replace(/\\/g, "/").toLowerCase() === normalizedRef);
10101
- return !matches;
10102
- }).join(`
10103
- `).trim()
10104
- }))
10105
- };
10106
- break;
10212
+ for (const r of results)
10213
+ if (r)
10214
+ summaries.push(...r);
10215
+ const failedBatches = errors.filter(Boolean);
10216
+ for (const error2 of failedBatches)
10217
+ debugError("Synthesis batch used deterministic fallback", error2);
10218
+ if (failedBatches.length) {
10219
+ cacheable = false;
10220
+ generationFallbacks.push(failedBatches.length + " synthesis batch fallback(s)");
10221
+ rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(failedBatches[0]) + "]", "warning");
10222
+ showProgressOverlay(rc.ctx, {
10223
+ phase: 3,
10224
+ phaseName: "Synthesize",
10225
+ detail: failedBatches.length + " batch fallback(s) \xB7 preserving coverage from deterministic evidence",
10226
+ explorationRounds
10227
+ });
10107
10228
  }
10108
- case "unsupported-claim":
10109
- canonical = removeUnsupportedClaim(canonical, gap.claim);
10110
- break;
10111
- case "inconsistency":
10112
- if (gap.detail.startsWith("blocked-none:"))
10113
- patchBlockedNone();
10114
- break;
10115
10229
  }
10116
- }
10117
- return renderSummary(canonical, { canonicalHeadings: true });
10118
- }
10119
- function hasUnclosedMarkdownFence(markdown) {
10120
- let open = null;
10121
- for (const line of markdown.split(/\r?\n/)) {
10122
- const match = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
10123
- if (!match)
10124
- continue;
10125
- const marker = match[1][0];
10126
- if (!open) {
10127
- open = { marker, length: match[1].length };
10128
- } else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
10129
- open = null;
10230
+ showProgressOverlay(rc.ctx, {
10231
+ phase: 3,
10232
+ phaseName: "Synthesize",
10233
+ detail: "Merging summaries with project continuity",
10234
+ model: rc.modelLabel,
10235
+ profile: rc.profile,
10236
+ extraction,
10237
+ explorationRounds,
10238
+ totalBatches: batches.length
10239
+ });
10240
+ method = "eesv";
10241
+ try {
10242
+ const r = await assembleLLM(summaries, extraction, explorationReport, rc.summaryModel, summaryAuth, pc.summaryBudgetTokens, rc.prevContext, rc.cancellation.signal, rc.services, rc.config.focusWeighting ? rc.focus : undefined, rc.previousState);
10243
+ if (r?.startsWith("##"))
10244
+ finalSummary = r;
10245
+ else
10246
+ throw new Error("Invalid summary response");
10247
+ } catch (err) {
10248
+ cacheable = false;
10249
+ generationFallbacks.push("assembly generation failed");
10250
+ debugError("Assembly used deterministic fallback", err);
10251
+ rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "warning");
10252
+ method = "heuristic";
10253
+ finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
10130
10254
  }
10131
10255
  }
10132
- return open !== null;
10133
- }
10134
- function patchResponseIsTruncated(patched, stopReason) {
10135
- const reason = String(stopReason ?? "");
10136
- return /(?:length|truncat|max(?:imum)?[_ -]?(?:output[_ -]?)?tokens?|token[_ -]?limit)/i.test(reason) || /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
10137
- }
10138
- function sectionIdentity(section) {
10139
- return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
10140
- }
10141
- async function patchSummary(summary, gaps, model, auth, signal, services) {
10142
- 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.
10143
-
10144
- Findings:
10145
- ` + gaps.map((gap, index) => index + 1 + ". " + formatVerificationGap(gap)).join(`
10146
- `) + `
10147
-
10148
- Current summary:
10149
- ` + summary + `
10150
-
10151
- Return the COMPLETE corrected summary in the same format.`;
10152
- try {
10153
- const maxTokens = Math.min(8192, getProviderCaps(model.provider).maxOutputTokens);
10154
- const response = await trackedComplete("patch", model, {
10155
- systemPrompt: COMPACT_SYSTEM_PREFIX,
10156
- messages: [
10157
- {
10158
- role: "user",
10159
- content: [{ type: "text", text: patchPrompt }],
10160
- timestamp: Date.now()
10161
- }
10162
- ]
10163
- }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens, signal }, services);
10164
- const patched = response.content.filter((content) => content.type === "text").map((content) => content.text).join(`
10165
- `).trim();
10166
- if (!patched.startsWith("##") || patchResponseIsTruncated(patched, response.stopReason))
10167
- return summary;
10168
- const originalSections = parseSummary(summary).sections;
10169
- const patchedSections = parseSummary(patched).sections;
10170
- const patchedBodies = new Map(patchedSections.map((section) => [
10171
- sectionIdentity(section),
10172
- section.body.trim()
10173
- ]));
10174
- const preserved = originalSections.every((section) => !section.body.trim() || Boolean(patchedBodies.get(sectionIdentity(section))));
10175
- return preserved ? patched : summary;
10176
- } catch (error2) {
10177
- debug("patchSummary LLM failed", error2);
10178
- return summary;
10256
+ Object.assign(rc, {
10257
+ finalSummary,
10258
+ method,
10259
+ methodForMetrics: method,
10260
+ generationFallbacks,
10261
+ llmCalls: rc.services.metrics.summary().totalCalls,
10262
+ summaries,
10263
+ explorationReport,
10264
+ explorationRounds,
10265
+ chunkCount
10266
+ });
10267
+ const out = advance(rc, "_synthesized");
10268
+ if (cacheable) {
10269
+ setCachedSynthesis(cacheKey, {
10270
+ finalSummary,
10271
+ method,
10272
+ summaries,
10273
+ explorationReport,
10274
+ explorationRounds,
10275
+ chunkCount
10276
+ });
10179
10277
  }
10278
+ markMeasuredPhase(out, "synthesize", synthPhaseStart);
10279
+ return out;
10180
10280
  }
10181
10281
 
10182
10282
  // src/app/steps/verify.ts
@@ -10292,57 +10392,6 @@ async function verifyAndPatch(rc) {
10292
10392
  // src/app/steps/state.ts
10293
10393
  import fs8 from "fs";
10294
10394
  import path12 from "path";
10295
-
10296
- // src/domain/yield-gate.ts
10297
- class YieldGateError extends Error {
10298
- reason;
10299
- name = "YieldGateError";
10300
- constructor(reason, estimate) {
10301
- super(reason === "target-miss" ? "Final summary estimate misses the planned compaction target" : "Final summary estimate does not meet the minimum saving policy");
10302
- this.reason = reason;
10303
- Object.assign(this, estimate);
10304
- }
10305
- plannedAfterTokens;
10306
- plannedSavedTokens;
10307
- plannedYield;
10308
- estimatedAfterTokens;
10309
- estimatedSavedTokens;
10310
- estimatedYield;
10311
- retainedTailTokens;
10312
- summaryTokens;
10313
- summaryBudgetTokens;
10314
- targetAfterTokens;
10315
- relaxedSoftBoundaries;
10316
- hardBoundaryAdjusted;
10317
- }
10318
- function verifyCompactionYield(totalTokens, summaryTokens, plan) {
10319
- const estimatedAfterTokens = plan.fixedContextTokens + plan.retainedTokens + summaryTokens;
10320
- const estimatedSavedTokens = Math.max(0, totalTokens - estimatedAfterTokens);
10321
- const estimatedYield = totalTokens > 0 ? estimatedSavedTokens / totalTokens : 0;
10322
- const estimate = {
10323
- plannedAfterTokens: plan.projectedAfterTokens,
10324
- plannedSavedTokens: plan.projectedSavedTokens,
10325
- plannedYield: plan.projectedYield,
10326
- estimatedAfterTokens,
10327
- estimatedSavedTokens,
10328
- estimatedYield,
10329
- retainedTailTokens: plan.retainedTokens,
10330
- summaryTokens,
10331
- summaryBudgetTokens: plan.summaryBudgetTokens,
10332
- targetAfterTokens: plan.targetAfterTokens,
10333
- relaxedSoftBoundaries: plan.relaxedSoftBoundaries,
10334
- hardBoundaryAdjusted: plan.hardBoundaryAdjusted
10335
- };
10336
- if (estimatedAfterTokens > plan.targetAfterTokens + ESTIMATOR_ROUNDING_TOLERANCE_TOKENS) {
10337
- throw new YieldGateError("target-miss", estimate);
10338
- }
10339
- if (estimatedYield < MIN_COMPACTION_SAVING_RATIO) {
10340
- throw new YieldGateError("insufficient-saving", estimate);
10341
- }
10342
- return estimate;
10343
- }
10344
-
10345
- // src/app/steps/state.ts
10346
10395
  function buildState(rc) {
10347
10396
  const extraction = rc.extraction;
10348
10397
  let summary = rc.finalSummary;
@@ -10404,7 +10453,8 @@ function buildState(rc) {
10404
10453
  compactionState = rc.services.scrubber.scrubValue(compactionState).value;
10405
10454
  const verificationEvidence = {
10406
10455
  sourceMessages: rc.llmMessages,
10407
- steering: { focus: rc.focus, note: rc.userNote }
10456
+ steering: { focus: rc.focus, note: rc.userNote },
10457
+ summaryBudgetTokens: rc.profileCfg?.summaryBudgetTokens ?? 6000
10408
10458
  };
10409
10459
  let postVerification = verifySummary(summary, extraction, compactionState, verificationEvidence);
10410
10460
  const postInitialScore = postVerification.score;
@@ -11233,11 +11283,14 @@ function aggregateProviderRoutes(metrics) {
11233
11283
  successes: 0,
11234
11284
  latency: 0,
11235
11285
  input: 0,
11236
- output: 0
11286
+ output: 0,
11287
+ failures: {}
11237
11288
  };
11238
11289
  group.calls++;
11239
11290
  if (metric.success)
11240
11291
  group.successes++;
11292
+ else if (metric.failureKind)
11293
+ group.failures[metric.failureKind] = (group.failures[metric.failureKind] ?? 0) + 1;
11241
11294
  group.latency += Math.max(0, metric.latencyMs);
11242
11295
  group.input += Math.max(0, metric.inputTokens) + Math.max(0, metric.cacheHitTokens) + Math.max(0, metric.cacheWriteTokens ?? 0);
11243
11296
  group.output += Math.max(0, metric.outputTokens);
@@ -11249,6 +11302,7 @@ function aggregateProviderRoutes(metrics) {
11249
11302
  model: group.model,
11250
11303
  calls: group.calls,
11251
11304
  successes: group.successes,
11305
+ ...Object.keys(group.failures).length ? { failures: group.failures } : {},
11252
11306
  avgLatencyMs: group.calls ? Math.round(group.latency / group.calls) : 0,
11253
11307
  inputTokens: group.input,
11254
11308
  outputTokens: group.output
@@ -11410,28 +11464,7 @@ async function recordFailureMetrics(rc, err, fields) {
11410
11464
  }
11411
11465
 
11412
11466
  // src/app/steps/persist.ts
11413
- import { convertToLlm as convertToLlm3 } from "@earendil-works/pi-coding-agent";
11414
-
11415
- // src/ui/error-format.ts
11416
- var MAX_ERROR_TEXT = 240;
11417
- var DEBUG_HINT = "Conversation unchanged. Set DEBUG=smart-compact for stack diagnostics.";
11418
- function compactText(error2) {
11419
- const text = error2 instanceof Error ? error2.message : String(error2);
11420
- return text.replace(/\s+/g, " ").trim().slice(0, MAX_ERROR_TEXT) || "Unknown error";
11421
- }
11422
- function formatCompactErrorForUi(error2) {
11423
- if (error2 instanceof VerificationGateError) {
11424
- const kinds = error2.gapKinds.slice(0, 4).join(", ") || "unknown";
11425
- return "Verification stopped apply at the " + error2.stage + " gate: " + error2.score + "/100, " + error2.gapCount + (error2.gapCount === 1 ? " unresolved gap [" : " unresolved gaps [") + kinds + "]. " + DEBUG_HINT;
11426
- }
11427
- if (error2 instanceof YieldGateError) {
11428
- const reason = error2.reason === "target-miss" ? "target missed" : "saving below 10%";
11429
- return "Yield check stopped apply: estimated " + error2.estimatedAfterTokens.toLocaleString() + "t after vs " + error2.targetAfterTokens.toLocaleString() + "t target (" + reason + "). " + DEBUG_HINT;
11430
- }
11431
- return "Smart compact failed: " + compactText(error2) + ". " + DEBUG_HINT;
11432
- }
11433
-
11434
- // src/app/steps/persist.ts
11467
+ import { convertToLlm as convertToLlm4 } from "@earendil-works/pi-coding-agent";
11435
11468
  async function persistAppliedState(pending) {
11436
11469
  if (!pending.projectId)
11437
11470
  return pending.compactionState || pending.extraction ? ["project state (project identity unavailable)"] : [];
@@ -11470,7 +11503,7 @@ async function commitAppliedCompaction(pending) {
11470
11503
  }
11471
11504
  function runDamageDetection(rc) {
11472
11505
  try {
11473
- const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm3([asBranchMessage(e.message)])).flat();
11506
+ const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm4([asBranchMessage(e.message)])).flat();
11474
11507
  if (postCompactMsgs.length <= 2)
11475
11508
  return;
11476
11509
  const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
@@ -11760,7 +11793,7 @@ async function runSmartCompact(opts) {
11760
11793
  const dur = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
11761
11794
  const hasPending = base.pendingRef.isPresent(runSessionId);
11762
11795
  if (hasPending || runFailed || finalRc)
11763
- base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : runFailed ? "Smart compact stopped safely in " + dur + " \xB7 no summary applied \xB7 Pi fallback continues" : "Smart compact run finished in " + dur, runFailed ? "warning" : "info");
11796
+ base.ctx.ui.notify(hasPending ? "Smart compact prepared in " + dur + " \u2014 awaiting native /compact" : runFailed ? "Smart compact stopped safely in " + dur + (base.flags.skipCompact ? " \xB7 no summary staged \xB7 context unchanged" : " \xB7 no summary applied \xB7 Pi fallback continues") : "Smart compact run finished in " + dur, runFailed ? "warning" : "info");
11764
11797
  }
11765
11798
  }
11766
11799
  }
@@ -13045,6 +13078,9 @@ function buildMetricsReport(entries = readMetricsLog(100), damageEntries, prebui
13045
13078
  "## Stage provider/model comparison",
13046
13079
  ...providerRoutes.length ? providerRoutes : ["- No stage-route evidence yet"],
13047
13080
  "",
13081
+ "## Recent provider call failures (not compaction outcomes)",
13082
+ ...entries.slice(-20).flatMap((entry) => (entry.providerRoutes ?? []).filter((route) => route.successes < route.calls).map((route) => "- " + route.stage + " / " + route.provider + "/" + route.model + ": " + (route.calls - route.successes) + "/" + route.calls + " calls failed; " + (route.failures ? JSON.stringify(route.failures) : "legacy cause unclassified"))),
13083
+ "",
13048
13084
  "## Failure taxonomy",
13049
13085
  ...Object.keys(insights.failures).length ? Object.entries(insights.failures).map(([kind, count]) => "- " + kind + ": " + count) : ["- No schema-v2 failures classified"]
13050
13086
  ].join(`
@@ -13293,7 +13329,7 @@ function registerSmartCompactTool(pi, dependencies) {
13293
13329
  pi.registerTool({
13294
13330
  name: "smart_compact",
13295
13331
  label: "Smart Compact",
13296
- description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Compacts the conversation into a structured summary preserving goals, decisions, open loops, modified files, and critical context. Call only when actual context usage is high; ignore pi-auto-context tool=XX% because that is tool-output ratio, not context fullness. The tool internally checks context usage and skips if not needed.",
13332
+ description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Prepares and stages a verified summary for the next /compact; this tool does not apply compaction mid-turn. The staged summary expires after 5 minutes. Call only when actual context usage is high; tool=XX% is tool-output ratio, not context fullness. Checks the configured context threshold before starting.",
13297
13333
  promptSnippet: "Smart compaction",
13298
13334
  promptGuidelines: [
13299
13335
  "Use only when actual context usage is high (for example pi-auto-context context>=60%).",
@@ -13374,7 +13410,7 @@ Dashboard: ` + dashboard : ""));
13374
13410
  const resolvedMode = mode ?? config.mode;
13375
13411
  const sessionId = resolveSessionId(ctx);
13376
13412
  if (!dryRun && pendingRef.peek(sessionId)?.sessionId === sessionId) {
13377
- return textResult("A smart summary is already staged for this session. The next /compact will use it; no LLM calls were made.");
13413
+ return textResult("A smart summary is already staged; context is unchanged. Run /compact before the 5-minute staging TTL expires to apply it. No LLM calls were made.");
13378
13414
  }
13379
13415
  const usage = ctx.getContextUsage?.();
13380
13416
  const totalTokens = usage?.tokens ?? 0;
@@ -13384,7 +13420,7 @@ Dashboard: ` + dashboard : ""));
13384
13420
  return textResult("Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + percent2 + "%). No action needed.");
13385
13421
  }
13386
13422
  if (contextPercent < config.minContextPercent) {
13387
- return textResult("Context is only " + percent2 + "% full (" + totalTokens.toLocaleString() + " tokens). Compaction is not needed yet. The tool=97% in status means tool output ratio, NOT context usage.");
13423
+ return textResult("Compaction skipped: context " + percent2 + "% (" + totalTokens.toLocaleString() + " / " + (ctx.model?.contextWindow ?? 0).toLocaleString() + " tokens), below the " + config.minContextPercent + "% agent-tool threshold. tool=XX% measures tool-output ratio, not context usage. " + "For deliberate early compaction, the user can run /smart-compact; preview and safety checks still apply.");
13388
13424
  }
13389
13425
  const current = ctx.model;
13390
13426
  const { segModel, sumModel, verifyModel } = resolveModels(ctx, current, config);
@@ -13417,7 +13453,7 @@ Dashboard: ` + dashboard : ""));
13417
13453
  content: [
13418
13454
  {
13419
13455
  type: "text",
13420
- text: "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 cached for " + Math.round(FIVE_MINUTES_MS / 60000) + " min. The next /compact will use it automatically."
13456
+ text: "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 staged, not applied, for " + Math.round(FIVE_MINUTES_MS / 60000) + " min. Context is unchanged. Run /compact within that time to apply it; expiry discards the candidate."
13421
13457
  }
13422
13458
  ],
13423
13459
  details: staged.details
@@ -15075,7 +15111,6 @@ function smartCompactExtension(pi) {
15075
15111
  onNativeApplyError,
15076
15112
  autoTriggered: true,
15077
15113
  overflowRecovery: event.reason === "overflow",
15078
- maxLlmCalls: Math.min(config.maxLlmCalls, AUTO_TRIGGER_MAX_LLM_CALLS),
15079
15114
  timeoutMs: effectiveTimeoutMs,
15080
15115
  abortSignal: event.signal,
15081
15116
  cancellationOut
@@ -15188,7 +15223,7 @@ function smartCompactExtension(pi) {
15188
15223
  pi.on("message_end", async (event, ctx) => {
15189
15224
  try {
15190
15225
  const sessionId = resolveSessionId(ctx);
15191
- const converted = convertToLlm4([event.message])[0];
15226
+ const converted = convertToLlm5([event.message])[0];
15192
15227
  if (!converted)
15193
15228
  return;
15194
15229
  const observation = damageMonitor.observe(sessionId, converted);
@@ -15220,7 +15255,7 @@ function smartCompactExtension(pi) {
15220
15255
  });
15221
15256
  }
15222
15257
  export {
15223
- smartCompactExtension as default,
15258
+ resolveModels,
15224
15259
  findModelById,
15225
- resolveModels
15260
+ smartCompactExtension as default
15226
15261
  };