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/ARCHITECTURE.md +21 -9
- package/CHANGELOG.md +26 -0
- package/README.md +27 -8
- package/dist/app/mode-policy.d.ts +3 -1
- package/dist/app/mode-policy.d.ts.map +1 -1
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/register-smart-compact-tool.d.ts.map +1 -1
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/prepare.d.ts.map +1 -1
- package/dist/app/steps/state.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/app/steps/window.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/domain/provider-evaluation.d.ts.map +1 -1
- package/dist/domain/telemetry.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +997 -962
- package/dist/infra/ai-messages.d.ts +6 -2
- package/dist/infra/ai-messages.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/provider-eval.js +224 -1
- package/dist/provider-scenario-eval.js +234 -9
- package/dist/telemetry-report.js +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/error-format.d.ts +3 -1
- package/dist/ui/error-format.d.ts.map +1 -1
- package/dist/ui/metrics-report.d.ts.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
6
|
+
convertToLlm as convertToLlm5
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
-
var VERSION = "9.6.
|
|
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
|
-
|
|
1865
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
6516
|
-
const line = " " + marker + MODE_LABELS[mode].padEnd(9) + recommendedMark + " " +
|
|
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 =
|
|
6610
|
-
const inputBudget =
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
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
|
-
|
|
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/
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
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
|
-
|
|
8882
|
-
|
|
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
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
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
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
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
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
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
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
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
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
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
|
|
9281
|
-
const
|
|
9282
|
-
|
|
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 (
|
|
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
|
|
9289
|
-
const
|
|
9290
|
-
|
|
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
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
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
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
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
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
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
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
|
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("
|
|
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
|
|
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 =
|
|
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
|
-
|
|
15258
|
+
resolveModels,
|
|
15224
15259
|
findModelById,
|
|
15225
|
-
|
|
15260
|
+
smartCompactExtension as default
|
|
15226
15261
|
};
|