pi-smart-compact 7.15.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +166 -283
- package/SECURITY.md +14 -5
- package/SUPPORT.md +18 -5
- 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/persist.d.ts.map +1 -1
- package/dist/app/steps/recover.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 +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/domain/summary-parse.d.ts +19 -6
- package/dist/domain/summary-parse.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +482 -133
- package/dist/infra/ai-messages.d.ts +40 -0
- package/dist/infra/ai-messages.d.ts.map +1 -0
- package/dist/infra/paths.d.ts +2 -0
- package/dist/infra/paths.d.ts.map +1 -1
- package/dist/phases/explore.d.ts +1 -1
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts +7 -0
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlays.d.ts +10 -2
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/damage.d.ts +14 -0
- package/dist/utils/damage.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +33 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/dist/utils/state.d.ts +7 -0
- package/dist/utils/state.d.ts.map +1 -1
- package/docs/RELEASE.md +1 -1
- package/docs/assets/banner.svg +71 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// src/constants.ts
|
|
19
|
-
var VERSION = "7.
|
|
19
|
+
var VERSION = "7.16.0", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
|
|
20
20
|
{PREV_CONTEXT}
|
|
21
21
|
|
|
22
22
|
{EXTRACTION_CONTEXT}
|
|
@@ -77,7 +77,8 @@ var init_constants = __esm(() => {
|
|
|
77
77
|
autoTriggerTimeoutMs: 120000,
|
|
78
78
|
backupEnabled: true,
|
|
79
79
|
backupDir: "",
|
|
80
|
-
minContextPercent: 60
|
|
80
|
+
minContextPercent: 60,
|
|
81
|
+
pinPaths: []
|
|
81
82
|
};
|
|
82
83
|
NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
|
|
83
84
|
SHIFT_RE = /simdi|peki|bide|bi de|gecelim|bakalim|yapalim|baska|sonra|tamam simdi|now let|also|next|let's|moving on|switch to/i;
|
|
@@ -254,6 +255,9 @@ function projectFingerprintFile(projectId) {
|
|
|
254
255
|
function compactionStateFile(projectId) {
|
|
255
256
|
return path.join(compactionStateDir(), projectId + ".json");
|
|
256
257
|
}
|
|
258
|
+
function remediationHintsFile(projectId) {
|
|
259
|
+
return path.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
|
|
260
|
+
}
|
|
257
261
|
function metricsDashboardFile() {
|
|
258
262
|
return path.join(cacheDir(), "smart-compact-report.html");
|
|
259
263
|
}
|
|
@@ -1434,9 +1438,12 @@ var DASHBOARD_PAGE_SIZE = 24;
|
|
|
1434
1438
|
var exports_overlays = {};
|
|
1435
1439
|
__export(exports_overlays, {
|
|
1436
1440
|
showResultScreen: () => showResultScreen,
|
|
1441
|
+
showRestorePicker: () => showRestorePicker,
|
|
1442
|
+
showRestoreAction: () => showRestoreAction,
|
|
1437
1443
|
showProgressOverlay: () => showProgressOverlay,
|
|
1438
1444
|
showMetricsDashboardUI: () => showMetricsDashboardUI,
|
|
1439
1445
|
showCompactUI: () => showCompactUI,
|
|
1446
|
+
showBackupViewer: () => showBackupViewer,
|
|
1440
1447
|
selectProfile: () => selectProfile,
|
|
1441
1448
|
selectModel: () => selectModel,
|
|
1442
1449
|
renderTokenBar: () => renderTokenBar,
|
|
@@ -1797,6 +1804,121 @@ async function showCompactUI(ctx, opts) {
|
|
|
1797
1804
|
return null;
|
|
1798
1805
|
return { model: selectedModel, profile: selectedProfile };
|
|
1799
1806
|
}
|
|
1807
|
+
async function showRestorePicker(ctx, backups) {
|
|
1808
|
+
const items = backups.map((b) => ({
|
|
1809
|
+
value: b.path,
|
|
1810
|
+
label: new Date(b.date).toLocaleString() + " \xB7 " + Math.max(1, Math.round(b.sizeBytes / 1024)) + "KB",
|
|
1811
|
+
description: b.sessionId.slice(0, 20)
|
|
1812
|
+
}));
|
|
1813
|
+
return await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
1814
|
+
const c = new Container;
|
|
1815
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1816
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Smart Compact \u2014 Restore")), 1, 0));
|
|
1817
|
+
c.addChild(new Text(theme.fg("dim", " Pick a backup to view its pre-compaction content"), 0, 0));
|
|
1818
|
+
c.addChild(new Text("", 0, 0));
|
|
1819
|
+
const sel = new SelectList(items, Math.min(items.length, 12), {
|
|
1820
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
1821
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
1822
|
+
description: (t) => theme.fg("muted", t),
|
|
1823
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
1824
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
1825
|
+
});
|
|
1826
|
+
sel.onSelect = (item) => done(item.value);
|
|
1827
|
+
sel.onCancel = () => done(null);
|
|
1828
|
+
c.addChild(sel);
|
|
1829
|
+
c.addChild(new Text("", 0, 0));
|
|
1830
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter view \xB7 esc cancel"), 0, 0));
|
|
1831
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1832
|
+
return {
|
|
1833
|
+
render: (w) => c.render(w),
|
|
1834
|
+
invalidate: () => c.invalidate(),
|
|
1835
|
+
handleInput: (d) => {
|
|
1836
|
+
sel.handleInput(d);
|
|
1837
|
+
tui.requestRender();
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
async function showBackupViewer(ctx, content, fp) {
|
|
1843
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
1844
|
+
const lines = content.split(`
|
|
1845
|
+
`);
|
|
1846
|
+
const pageSize = 40;
|
|
1847
|
+
let scroll = 0;
|
|
1848
|
+
const maxScroll = Math.max(0, lines.length - pageSize);
|
|
1849
|
+
return {
|
|
1850
|
+
render: (w) => {
|
|
1851
|
+
const out = [
|
|
1852
|
+
truncateToWidth(theme.fg("accent", theme.bold(" \u21A9 Restored backup")) + theme.fg("dim", " \xB7 " + lines.length + " lines \xB7 " + Math.max(1, Math.round(content.length / 1024)) + "KB"), w),
|
|
1853
|
+
truncateToWidth(theme.fg("dim", " " + fp), w),
|
|
1854
|
+
truncateToWidth(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, w))), w)
|
|
1855
|
+
];
|
|
1856
|
+
for (const line of lines.slice(scroll, scroll + pageSize)) {
|
|
1857
|
+
out.push(truncateToWidth(theme.fg("text", line), w));
|
|
1858
|
+
}
|
|
1859
|
+
if (lines.length > pageSize) {
|
|
1860
|
+
out.push(truncateToWidth(theme.fg("dim", " showing " + (scroll + 1) + "\u2013" + Math.min(lines.length, scroll + pageSize) + " of " + lines.length), w));
|
|
1861
|
+
}
|
|
1862
|
+
out.push("", truncateToWidth(theme.fg("dim", " \u2191\u2193 scroll \xB7 pgup/pgdn \xB7 home/end \xB7 esc/q close"), w));
|
|
1863
|
+
return out;
|
|
1864
|
+
},
|
|
1865
|
+
invalidate: () => {},
|
|
1866
|
+
handleInput: (data) => {
|
|
1867
|
+
if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
|
|
1868
|
+
done(undefined);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
if (matchesKey(data, Key.home))
|
|
1872
|
+
scroll = 0;
|
|
1873
|
+
else if (matchesKey(data, Key.end))
|
|
1874
|
+
scroll = maxScroll;
|
|
1875
|
+
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
1876
|
+
scroll = Math.max(0, scroll - pageSize);
|
|
1877
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
1878
|
+
scroll = Math.min(maxScroll, scroll + pageSize);
|
|
1879
|
+
else if (keybindings.matches(data, "tui.select.up"))
|
|
1880
|
+
scroll = Math.max(0, scroll - 1);
|
|
1881
|
+
else if (keybindings.matches(data, "tui.select.down"))
|
|
1882
|
+
scroll = Math.min(maxScroll, scroll + 1);
|
|
1883
|
+
tui.requestRender();
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
}, { overlay: true, overlayOptions: { width: "85%", anchor: "center", maxHeight: "85%" } });
|
|
1887
|
+
}
|
|
1888
|
+
async function showRestoreAction(ctx, backupPath) {
|
|
1889
|
+
const items = [
|
|
1890
|
+
{ value: "view", label: "View content", description: "Read the pre-compaction conversation" },
|
|
1891
|
+
{ value: "restore", label: "Restore into a new session", description: "Fork from here + inject this backup as context" }
|
|
1892
|
+
];
|
|
1893
|
+
return await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
1894
|
+
const c = new Container;
|
|
1895
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1896
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \u21A9 Restore action")), 1, 0));
|
|
1897
|
+
c.addChild(new Text(theme.fg("dim", " " + backupPath), 0, 0));
|
|
1898
|
+
c.addChild(new Text("", 0, 0));
|
|
1899
|
+
const sel = new SelectList(items, 2, {
|
|
1900
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
1901
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
1902
|
+
description: (t) => theme.fg("muted", t),
|
|
1903
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
1904
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
1905
|
+
});
|
|
1906
|
+
sel.onSelect = (item) => done(item.value);
|
|
1907
|
+
sel.onCancel = () => done(null);
|
|
1908
|
+
c.addChild(sel);
|
|
1909
|
+
c.addChild(new Text("", 0, 0));
|
|
1910
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \xB7 enter select \xB7 esc cancel"), 0, 0));
|
|
1911
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
1912
|
+
return {
|
|
1913
|
+
render: (w) => c.render(w),
|
|
1914
|
+
invalidate: () => c.invalidate(),
|
|
1915
|
+
handleInput: (d) => {
|
|
1916
|
+
sel.handleInput(d);
|
|
1917
|
+
tui.requestRender();
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1800
1922
|
var init_overlays = __esm(() => {
|
|
1801
1923
|
init_cache();
|
|
1802
1924
|
init_tokens();
|
|
@@ -1883,6 +2005,16 @@ function validateSmartCompactConfig(sc) {
|
|
|
1883
2005
|
delete sc.minContextPercent;
|
|
1884
2006
|
}
|
|
1885
2007
|
}
|
|
2008
|
+
if ("backupDir" in sc && sc.backupDir !== undefined && typeof sc.backupDir !== "string") {
|
|
2009
|
+
warn("smart-compact config: backupDir must be a string, got " + typeof sc.backupDir + ". Using default.");
|
|
2010
|
+
delete sc.backupDir;
|
|
2011
|
+
}
|
|
2012
|
+
if ("pinPaths" in sc && sc.pinPaths !== undefined) {
|
|
2013
|
+
if (!Array.isArray(sc.pinPaths) || !sc.pinPaths.every((x) => typeof x === "string")) {
|
|
2014
|
+
warn("smart-compact config: pinPaths must be a string[], ignoring.");
|
|
2015
|
+
delete sc.pinPaths;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1886
2018
|
}
|
|
1887
2019
|
var _cfg = null;
|
|
1888
2020
|
var _cfgMtime = 0;
|
|
@@ -1974,6 +2106,68 @@ function backupConversation(convText, sessionId) {
|
|
|
1974
2106
|
return null;
|
|
1975
2107
|
}
|
|
1976
2108
|
}
|
|
2109
|
+
function listBackups(limit = 20) {
|
|
2110
|
+
try {
|
|
2111
|
+
const dir = loadConfig().backupDir;
|
|
2112
|
+
if (!fs2.existsSync(dir))
|
|
2113
|
+
return [];
|
|
2114
|
+
const out = [];
|
|
2115
|
+
for (const name of fs2.readdirSync(dir)) {
|
|
2116
|
+
if (!name.endsWith(".md"))
|
|
2117
|
+
continue;
|
|
2118
|
+
const full = path3.join(dir, name);
|
|
2119
|
+
try {
|
|
2120
|
+
const stat = fs2.statSync(full);
|
|
2121
|
+
if (!stat.isFile())
|
|
2122
|
+
continue;
|
|
2123
|
+
const head = fs2.readFileSync(full, "utf-8").split(`
|
|
2124
|
+
`).slice(0, 5).join(`
|
|
2125
|
+
`);
|
|
2126
|
+
const date = head.match(/^# Date:\s*(.+)$/m)?.[1]?.trim();
|
|
2127
|
+
const session = head.match(/^# Session:\s*(.+)$/m)?.[1]?.trim();
|
|
2128
|
+
out.push({
|
|
2129
|
+
path: full,
|
|
2130
|
+
sessionId: session ?? name,
|
|
2131
|
+
date: date ?? stat.mtime.toISOString(),
|
|
2132
|
+
sizeBytes: stat.size
|
|
2133
|
+
});
|
|
2134
|
+
} catch {}
|
|
2135
|
+
}
|
|
2136
|
+
out.sort((a, b) => a.date < b.date ? 1 : a.date > b.date ? -1 : 0);
|
|
2137
|
+
return out.slice(0, limit);
|
|
2138
|
+
} catch (e) {
|
|
2139
|
+
warn("listBackups failed", e);
|
|
2140
|
+
return [];
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function readBackupContent(fp) {
|
|
2144
|
+
try {
|
|
2145
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
2146
|
+
const lines = raw.split(`
|
|
2147
|
+
`);
|
|
2148
|
+
let i = 0;
|
|
2149
|
+
while (i < lines.length && lines[i].startsWith("#"))
|
|
2150
|
+
i++;
|
|
2151
|
+
if (i < lines.length && lines[i].trim() === "")
|
|
2152
|
+
i++;
|
|
2153
|
+
return lines.slice(i).join(`
|
|
2154
|
+
`).trim() || null;
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
warn("readBackupContent failed", e);
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function buildRestoreMessage(content, source) {
|
|
2161
|
+
return {
|
|
2162
|
+
customType: "smart-compact-restore",
|
|
2163
|
+
content: `# Restored pre-compaction context (smart-compact backup)
|
|
2164
|
+
Source: ` + source + `
|
|
2165
|
+
|
|
2166
|
+
` + content,
|
|
2167
|
+
display: true,
|
|
2168
|
+
details: { source, restoredAt: Date.now() }
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
1977
2171
|
function getPreviousCompactionContext(branch) {
|
|
1978
2172
|
const compactions = branch.filter((e) => e.type === "compaction");
|
|
1979
2173
|
if (!compactions.length)
|
|
@@ -2009,7 +2203,7 @@ function branchIndexToMsgIndex(branchEntries, branchIdx, msgs) {
|
|
|
2009
2203
|
msgCount++;
|
|
2010
2204
|
}
|
|
2011
2205
|
}
|
|
2012
|
-
return Math.min(msgCount - 1, msgs.length - 1);
|
|
2206
|
+
return Math.max(0, Math.min(msgCount - 1, msgs.length - 1));
|
|
2013
2207
|
}
|
|
2014
2208
|
function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
|
|
2015
2209
|
let adjusted = keepFromIndex;
|
|
@@ -2216,8 +2410,6 @@ function computeToolCharPercentage(branchEntries) {
|
|
|
2216
2410
|
const block = part;
|
|
2217
2411
|
if (block.type === "text" && typeof block.text === "string")
|
|
2218
2412
|
mc += block.text.length;
|
|
2219
|
-
else if (block.type === "text" && typeof block.content === "string")
|
|
2220
|
-
mc += block.content.length;
|
|
2221
2413
|
}
|
|
2222
2414
|
}
|
|
2223
2415
|
totalChars += mc;
|
|
@@ -2918,12 +3110,17 @@ function resolveCompactionWindow(rc) {
|
|
|
2918
3110
|
}
|
|
2919
3111
|
}
|
|
2920
3112
|
keepFrom = smartKeepBoundary(msgs, keepFrom, branch);
|
|
3113
|
+
if (keepFrom >= msgs.length)
|
|
3114
|
+
keepFrom = msgs.length - 1;
|
|
2921
3115
|
keepFrom = guardToolCallBoundary(msgs, keepFrom);
|
|
3116
|
+
if (msgs[keepFrom]?.message?.role === "toolResult") {
|
|
3117
|
+
return null;
|
|
3118
|
+
}
|
|
2922
3119
|
const toCompact = msgs.slice(0, keepFrom);
|
|
2923
3120
|
if (!toCompact.length)
|
|
2924
3121
|
return null;
|
|
2925
3122
|
const contextPercent = rc.ctx.model && totalTokens ? totalTokens / rc.ctx.model.contextWindow * 100 : 0;
|
|
2926
|
-
const firstKeptId = msgs[keepFrom]
|
|
3123
|
+
const firstKeptId = msgs[keepFrom].id;
|
|
2927
3124
|
const sessionId = resolveSessionId(rc.ctx);
|
|
2928
3125
|
const out = rc;
|
|
2929
3126
|
out.sessionId = sessionId;
|
|
@@ -2942,6 +3139,14 @@ function resolveCompactionWindow(rc) {
|
|
|
2942
3139
|
// src/app/steps/recover.ts
|
|
2943
3140
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2944
3141
|
|
|
3142
|
+
// src/infra/ai-messages.ts
|
|
3143
|
+
function asBranchMessage(message) {
|
|
3144
|
+
return message;
|
|
3145
|
+
}
|
|
3146
|
+
function asSerializableMessages(msgs) {
|
|
3147
|
+
return msgs;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
2945
3150
|
// src/utils/session-log.ts
|
|
2946
3151
|
import * as fs4 from "fs";
|
|
2947
3152
|
import * as path7 from "path";
|
|
@@ -3054,17 +3259,19 @@ function findSessionLogFile(sessionId) {
|
|
|
3054
3259
|
}
|
|
3055
3260
|
return remember(null);
|
|
3056
3261
|
}
|
|
3057
|
-
function normalizeLogMessage(msg) {
|
|
3262
|
+
function normalizeLogMessage(msg, entryTimestamp) {
|
|
3058
3263
|
if (!msg || !msg.role)
|
|
3059
3264
|
return null;
|
|
3060
3265
|
const role = msg.role;
|
|
3061
3266
|
if (role === "user" || role === "assistant" || role === "toolResult") {
|
|
3267
|
+
const ts = entryTimestamp ? Date.parse(entryTimestamp) : NaN;
|
|
3062
3268
|
return {
|
|
3063
3269
|
role,
|
|
3064
3270
|
content: msg.content,
|
|
3065
3271
|
isError: msg.isError,
|
|
3066
3272
|
toolCallId: msg.toolCallId,
|
|
3067
|
-
|
|
3273
|
+
toolName: msg.toolName,
|
|
3274
|
+
timestamp: Number.isFinite(ts) ? ts : undefined
|
|
3068
3275
|
};
|
|
3069
3276
|
}
|
|
3070
3277
|
return null;
|
|
@@ -3095,7 +3302,7 @@ function readOriginalMessageMap(sessionId) {
|
|
|
3095
3302
|
continue;
|
|
3096
3303
|
}
|
|
3097
3304
|
if (entry.type === "message" && entry.id && entry.message) {
|
|
3098
|
-
const normalized = normalizeLogMessage(entry.message);
|
|
3305
|
+
const normalized = normalizeLogMessage(entry.message, entry.timestamp);
|
|
3099
3306
|
if (normalized)
|
|
3100
3307
|
map.set(entry.id, normalized);
|
|
3101
3308
|
}
|
|
@@ -3143,7 +3350,7 @@ function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
|
3143
3350
|
|
|
3144
3351
|
// src/app/steps/recover.ts
|
|
3145
3352
|
function recoverSessionLog(rc) {
|
|
3146
|
-
let llmMessages = convertToLlm(rc.toCompact.map((e) => e.message));
|
|
3353
|
+
let llmMessages = convertToLlm(rc.toCompact.map((e) => asBranchMessage(e.message)));
|
|
3147
3354
|
if (hasTruncatedMessages(llmMessages)) {
|
|
3148
3355
|
const fromLog = resolveCompactionMessages(rc.sessionId, rc.toCompact);
|
|
3149
3356
|
if (fromLog) {
|
|
@@ -3546,7 +3753,7 @@ function extractWithCache(rc) {
|
|
|
3546
3753
|
const pruneEnd = Date.now();
|
|
3547
3754
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
3548
3755
|
const extractionStart = pruneEnd;
|
|
3549
|
-
const convText = serializeConversation(rc.llmMessages);
|
|
3756
|
+
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
3550
3757
|
const convTokens = estimateTokens(convText);
|
|
3551
3758
|
const backupPath = backupConversation(convText, rc.sessionId);
|
|
3552
3759
|
const prevContext = getPreviousCompactionContext(rc.branch);
|
|
@@ -3617,6 +3824,7 @@ function extractWithCache(rc) {
|
|
|
3617
3824
|
init_overlays();
|
|
3618
3825
|
|
|
3619
3826
|
// src/phases/explore.ts
|
|
3827
|
+
import { Type } from "typebox";
|
|
3620
3828
|
init_constants();
|
|
3621
3829
|
init_cache();
|
|
3622
3830
|
init_tokens();
|
|
@@ -3639,32 +3847,32 @@ var EXPLORATION_TOOLS = [
|
|
|
3639
3847
|
{
|
|
3640
3848
|
name: "get_message_range",
|
|
3641
3849
|
description: "Get compact summaries of messages from start to end index (0-based).",
|
|
3642
|
-
parameters: {
|
|
3850
|
+
parameters: Type.Object({ start: Type.Number(), end: Type.Number() })
|
|
3643
3851
|
},
|
|
3644
3852
|
{
|
|
3645
3853
|
name: "search_conversation",
|
|
3646
3854
|
description: "Search for text in conversation messages.",
|
|
3647
|
-
parameters: {
|
|
3855
|
+
parameters: Type.Object({ query: Type.String() })
|
|
3648
3856
|
},
|
|
3649
3857
|
{
|
|
3650
3858
|
name: "get_recent_user_messages",
|
|
3651
3859
|
description: "Get the last N user messages.",
|
|
3652
|
-
parameters: {
|
|
3860
|
+
parameters: Type.Object({ count: Type.Optional(Type.Number()) })
|
|
3653
3861
|
},
|
|
3654
3862
|
{
|
|
3655
3863
|
name: "get_context_around",
|
|
3656
3864
|
description: "Get context around a specific message index.",
|
|
3657
|
-
parameters: {
|
|
3865
|
+
parameters: Type.Object({ index: Type.Number(), radius: Type.Optional(Type.Number()) })
|
|
3658
3866
|
},
|
|
3659
3867
|
{
|
|
3660
3868
|
name: "get_file_changes",
|
|
3661
3869
|
description: "Get tool calls that modified a specific file.",
|
|
3662
|
-
parameters: {
|
|
3870
|
+
parameters: Type.Object({ path: Type.String() })
|
|
3663
3871
|
},
|
|
3664
3872
|
{
|
|
3665
3873
|
name: "get_error_chain",
|
|
3666
3874
|
description: "Get all messages related to a specific error.",
|
|
3667
|
-
parameters: {
|
|
3875
|
+
parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
|
|
3668
3876
|
}
|
|
3669
3877
|
];
|
|
3670
3878
|
function executeExplorationTool(call, llmMessages) {
|
|
@@ -3761,56 +3969,72 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
3761
3969
|
const md = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
3762
3970
|
if (md)
|
|
3763
3971
|
json = md[1].trim();
|
|
3764
|
-
|
|
3765
|
-
if (
|
|
3972
|
+
const startIdx = json.indexOf("{"), endIdx = json.lastIndexOf("}");
|
|
3973
|
+
if (startIdx === -1 || endIdx === -1)
|
|
3766
3974
|
return fallbackExplorationReport(llmMessages);
|
|
3767
|
-
|
|
3975
|
+
const rawJson = json.slice(startIdx, endIdx + 1);
|
|
3768
3976
|
try {
|
|
3769
3977
|
return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
|
|
3770
|
-
} catch (
|
|
3771
|
-
debug("JSON parse attempt 1 failed",
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
debug("JSON parse attempt 1 failed", err);
|
|
3772
3980
|
}
|
|
3773
3981
|
const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3774
3982
|
try {
|
|
3775
3983
|
return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
|
|
3776
|
-
} catch (
|
|
3777
|
-
debug("JSON parse attempt 2 (cleaned) failed",
|
|
3984
|
+
} catch (err) {
|
|
3985
|
+
debug("JSON parse attempt 2 (cleaned) failed", err);
|
|
3778
3986
|
}
|
|
3779
3987
|
const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
|
|
3780
3988
|
if (boundaryMatch) {
|
|
3781
3989
|
try {
|
|
3782
3990
|
const boundaries = JSON.parse("[" + boundaryMatch[1] + "]");
|
|
3783
|
-
return { ...fallbackExplorationReport(llmMessages), boundaries: boundaries
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
3787
|
-
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
3788
|
-
})) };
|
|
3789
|
-
} catch (e2) {
|
|
3790
|
-
debug("Boundary JSON parse failed", e2);
|
|
3991
|
+
return { ...fallbackExplorationReport(llmMessages), boundaries: normalizeBoundaries(boundaries, llmMessages.length) };
|
|
3992
|
+
} catch (err) {
|
|
3993
|
+
debug("Boundary JSON parse failed", err);
|
|
3791
3994
|
}
|
|
3792
3995
|
}
|
|
3793
3996
|
return fallbackExplorationReport(llmMessages);
|
|
3794
3997
|
}
|
|
3998
|
+
var BOUNDARY_PRIORITIES = ["critical", "high", "normal", "low"];
|
|
3999
|
+
var SESSION_TYPES = ["implementation", "review", "debugging", "discussion"];
|
|
4000
|
+
function stringArray(v) {
|
|
4001
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
4002
|
+
}
|
|
4003
|
+
function normalizeBoundaries(raw, llmLength) {
|
|
4004
|
+
if (!Array.isArray(raw))
|
|
4005
|
+
return [];
|
|
4006
|
+
const maxIndex = Math.max(0, llmLength - 2);
|
|
4007
|
+
return raw.filter((b) => !!b && typeof b === "object" && typeof b.afterIndex === "number").map((b) => {
|
|
4008
|
+
const priority = b.priority;
|
|
4009
|
+
const confidence = b.confidence;
|
|
4010
|
+
return {
|
|
4011
|
+
afterIndex: Math.max(0, Math.min(b.afterIndex, maxIndex)),
|
|
4012
|
+
topic: String(b.topic ?? "").slice(0, 100),
|
|
4013
|
+
priority: typeof priority === "string" && BOUNDARY_PRIORITIES.includes(priority) ? priority : "normal",
|
|
4014
|
+
confidence: typeof confidence === "number" && Number.isFinite(confidence) ? Math.min(1, Math.max(0, confidence)) : 0.5
|
|
4015
|
+
};
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
3795
4018
|
function buildExplorationReportFromParsed(parsed, llmMessages) {
|
|
4019
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4020
|
+
return fallbackExplorationReport(llmMessages);
|
|
4021
|
+
}
|
|
4022
|
+
const p = parsed;
|
|
4023
|
+
const statusAssessment = p.statusAssessment ?? null;
|
|
4024
|
+
const sessionTypeRaw = p.sessionType;
|
|
3796
4025
|
return {
|
|
3797
|
-
boundaries: (
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
})),
|
|
3803
|
-
mainGoal: parsed.mainGoal ?? "",
|
|
3804
|
-
sessionType: ["implementation", "review", "debugging", "discussion"].includes(parsed.sessionType) ? parsed.sessionType : "implementation",
|
|
3805
|
-
enrichedConstraints: Array.isArray(parsed.enrichedConstraints) ? parsed.enrichedConstraints.map(String) : [],
|
|
3806
|
-
crossReferences: Array.isArray(parsed.crossReferences) ? parsed.crossReferences.map(String) : [],
|
|
4026
|
+
boundaries: normalizeBoundaries(p.boundaries, llmMessages.length),
|
|
4027
|
+
mainGoal: typeof p.mainGoal === "string" ? p.mainGoal : "",
|
|
4028
|
+
sessionType: typeof sessionTypeRaw === "string" && SESSION_TYPES.includes(sessionTypeRaw) ? sessionTypeRaw : "implementation",
|
|
4029
|
+
enrichedConstraints: stringArray(p.enrichedConstraints),
|
|
4030
|
+
crossReferences: stringArray(p.crossReferences),
|
|
3807
4031
|
statusAssessment: {
|
|
3808
|
-
done:
|
|
3809
|
-
inProgress:
|
|
3810
|
-
blocked:
|
|
4032
|
+
done: stringArray(statusAssessment?.done),
|
|
4033
|
+
inProgress: stringArray(statusAssessment?.inProgress),
|
|
4034
|
+
blocked: stringArray(statusAssessment?.blocked)
|
|
3811
4035
|
},
|
|
3812
|
-
criticalContext:
|
|
3813
|
-
keyDecisions:
|
|
4036
|
+
criticalContext: stringArray(p.criticalContext),
|
|
4037
|
+
keyDecisions: stringArray(p.keyDecisions)
|
|
3814
4038
|
};
|
|
3815
4039
|
}
|
|
3816
4040
|
function fallbackExplorationReport(llmMessages) {
|
|
@@ -3878,7 +4102,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3878
4102
|
toolSupport.set(cacheKey, true, svc.clock.now());
|
|
3879
4103
|
const messages = [
|
|
3880
4104
|
{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() },
|
|
3881
|
-
|
|
4105
|
+
probeResp
|
|
3882
4106
|
];
|
|
3883
4107
|
for (const tc of toolCalls) {
|
|
3884
4108
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
@@ -3912,7 +4136,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3912
4136
|
}
|
|
3913
4137
|
return { report: report2, rounds, toolSupported: true };
|
|
3914
4138
|
}
|
|
3915
|
-
messages.push(
|
|
4139
|
+
messages.push(response);
|
|
3916
4140
|
for (const tc of nextToolCalls) {
|
|
3917
4141
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
3918
4142
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
@@ -4228,6 +4452,19 @@ function assembleFallback(summaries, extraction) {
|
|
|
4228
4452
|
].join(`
|
|
4229
4453
|
`);
|
|
4230
4454
|
}
|
|
4455
|
+
function failedChunkSummary(ch) {
|
|
4456
|
+
return {
|
|
4457
|
+
topic: ch.topic,
|
|
4458
|
+
startIndex: ch.startIndex,
|
|
4459
|
+
endIndex: ch.endIndex,
|
|
4460
|
+
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
4461
|
+
`).slice(0, 300),
|
|
4462
|
+
keyDecisions: [],
|
|
4463
|
+
filesModified: [],
|
|
4464
|
+
filesRead: [],
|
|
4465
|
+
priority: ch.priority
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4231
4468
|
|
|
4232
4469
|
// src/app/steps/synthesize.ts
|
|
4233
4470
|
init_constants();
|
|
@@ -4346,10 +4583,15 @@ async function summarizeConversation(rc) {
|
|
|
4346
4583
|
}
|
|
4347
4584
|
const concurrency = rc.providerCaps.concurrencyLimit;
|
|
4348
4585
|
if (totalBatches <= 1) {
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4586
|
+
const single = batches[0];
|
|
4587
|
+
if (single) {
|
|
4588
|
+
try {
|
|
4589
|
+
summaries.push(...await summarizeBatch(single, extraction, rc.summaryModel, rc.summaryAuth, rc.cancellation.signal, rc.services));
|
|
4590
|
+
} catch (err) {
|
|
4591
|
+
summaries.push(...single.map((ch) => failedChunkSummary(ch)));
|
|
4592
|
+
}
|
|
4593
|
+
} else {
|
|
4594
|
+
rc.vlog("Synthesize: 0 batches \u2014 skipping summarization, using fallback assembly");
|
|
4353
4595
|
}
|
|
4354
4596
|
} else {
|
|
4355
4597
|
const results = new Array(totalBatches);
|
|
@@ -4426,19 +4668,6 @@ async function summarizeConversation(rc) {
|
|
|
4426
4668
|
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
4427
4669
|
return advance(out, "_synthesized");
|
|
4428
4670
|
}
|
|
4429
|
-
function failedChunkSummary(ch) {
|
|
4430
|
-
return {
|
|
4431
|
-
topic: ch.topic,
|
|
4432
|
-
startIndex: ch.startIndex,
|
|
4433
|
-
endIndex: ch.endIndex,
|
|
4434
|
-
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
4435
|
-
`).slice(0, 300),
|
|
4436
|
-
keyDecisions: [],
|
|
4437
|
-
filesModified: [],
|
|
4438
|
-
filesRead: [],
|
|
4439
|
-
priority: ch.priority
|
|
4440
|
-
};
|
|
4441
|
-
}
|
|
4442
4671
|
|
|
4443
4672
|
// src/phases/verify.ts
|
|
4444
4673
|
init_constants();
|
|
@@ -4582,7 +4811,7 @@ function renderSummary(summary, opts = {}) {
|
|
|
4582
4811
|
`).trim() + `
|
|
4583
4812
|
`;
|
|
4584
4813
|
}
|
|
4585
|
-
function upsertSection(summary, kind, body,
|
|
4814
|
+
function upsertSection(summary, kind, body, placement) {
|
|
4586
4815
|
const heading = canonicalHeading(kind);
|
|
4587
4816
|
const existing = summary.sections.findIndex((s) => s.kind === kind);
|
|
4588
4817
|
if (existing >= 0) {
|
|
@@ -4590,15 +4819,30 @@ function upsertSection(summary, kind, body, before) {
|
|
|
4590
4819
|
sections[existing] = { kind, heading, body: body.trim() };
|
|
4591
4820
|
return { sections };
|
|
4592
4821
|
}
|
|
4822
|
+
const hint = placement == null ? {} : typeof placement === "string" ? { before: placement } : placement;
|
|
4593
4823
|
const section = { kind, heading, body: body.trim() };
|
|
4594
|
-
if (before) {
|
|
4595
|
-
const idx = summary.sections.findIndex((s) => s.kind === before);
|
|
4824
|
+
if (hint.before) {
|
|
4825
|
+
const idx = summary.sections.findIndex((s) => s.kind === hint.before);
|
|
4596
4826
|
if (idx >= 0) {
|
|
4597
4827
|
const sections = summary.sections.slice();
|
|
4598
4828
|
sections.splice(idx, 0, section);
|
|
4599
4829
|
return { sections };
|
|
4600
4830
|
}
|
|
4601
4831
|
}
|
|
4832
|
+
if (hint.after) {
|
|
4833
|
+
let idx = -1;
|
|
4834
|
+
for (let i = summary.sections.length - 1;i >= 0; i--) {
|
|
4835
|
+
if (summary.sections[i].kind === hint.after) {
|
|
4836
|
+
idx = i;
|
|
4837
|
+
break;
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
if (idx >= 0) {
|
|
4841
|
+
const sections = summary.sections.slice();
|
|
4842
|
+
sections.splice(idx + 1, 0, section);
|
|
4843
|
+
return { sections };
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4602
4846
|
return { sections: [...summary.sections, section] };
|
|
4603
4847
|
}
|
|
4604
4848
|
function appendToSection(summary, kind, text, fallbackBody = "") {
|
|
@@ -4895,6 +5139,10 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4895
5139
|
let decisionId = 0;
|
|
4896
5140
|
let constraintId = 0;
|
|
4897
5141
|
let errorId = 0;
|
|
5142
|
+
const modifiedBasenames = extraction.modifiedFiles.map((f) => ({
|
|
5143
|
+
path: f.path,
|
|
5144
|
+
bn: f.path.split("/").pop()?.toLowerCase() ?? ""
|
|
5145
|
+
}));
|
|
4898
5146
|
return {
|
|
4899
5147
|
goal: extraction.mainGoal,
|
|
4900
5148
|
decisions: extraction.decisions.map((d) => ({
|
|
@@ -4913,7 +5161,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4913
5161
|
readFiles: extraction.readFiles,
|
|
4914
5162
|
deletedFiles: extraction.deletedFiles,
|
|
4915
5163
|
unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
|
|
4916
|
-
const
|
|
5164
|
+
const msgLower = e.message.toLowerCase();
|
|
5165
|
+
const bn = modifiedBasenames.find((f) => f.bn.length > 0 && msgLower.includes(f.bn));
|
|
4917
5166
|
return {
|
|
4918
5167
|
id: "error-" + ++errorId,
|
|
4919
5168
|
message: e.message.slice(0, 300),
|
|
@@ -5028,7 +5277,20 @@ function injectDeltaSection(summary, delta) {
|
|
|
5028
5277
|
return summary;
|
|
5029
5278
|
const parsed = parseSummary(summary);
|
|
5030
5279
|
const hasOpenLoops = parsed.sections.some((s) => s.kind === "open-loops");
|
|
5031
|
-
const
|
|
5280
|
+
const placement = hasOpenLoops ? { after: "open-loops" } : { before: "next-steps" };
|
|
5281
|
+
const updated = upsertSection(parsed, "changes", body, placement);
|
|
5282
|
+
return renderSummary(updated);
|
|
5283
|
+
}
|
|
5284
|
+
function ensurePinnedPaths(summary, pinned) {
|
|
5285
|
+
if (!pinned.length)
|
|
5286
|
+
return summary;
|
|
5287
|
+
const lower = summary.toLowerCase();
|
|
5288
|
+
const missing = pinned.filter((p) => p && p.trim().length > 0 && !lower.includes(p.toLowerCase()));
|
|
5289
|
+
if (!missing.length)
|
|
5290
|
+
return summary;
|
|
5291
|
+
const parsed = parseSummary(summary);
|
|
5292
|
+
const updated = appendToSection(parsed, "files-read", missing.map((p) => "- " + p).join(`
|
|
5293
|
+
`), "- Pinned by config (always preserved):");
|
|
5032
5294
|
return renderSummary(updated);
|
|
5033
5295
|
}
|
|
5034
5296
|
function extractNextActions(summary) {
|
|
@@ -5046,62 +5308,6 @@ function extractCriticalContext(summary) {
|
|
|
5046
5308
|
`).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5047
5309
|
}
|
|
5048
5310
|
|
|
5049
|
-
// src/app/steps/state.ts
|
|
5050
|
-
init_tokens();
|
|
5051
|
-
function buildState(rc) {
|
|
5052
|
-
const extraction = rc.extraction;
|
|
5053
|
-
let summary = rc.finalSummary;
|
|
5054
|
-
const openLoops = extractOpenLoops(rc.llmMessages, extraction);
|
|
5055
|
-
if (openLoops.length > 0) {
|
|
5056
|
-
rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
5057
|
-
summary = injectOpenLoopsSection(summary, openLoops);
|
|
5058
|
-
}
|
|
5059
|
-
const nextActions = extractNextActions(summary);
|
|
5060
|
-
const criticalContextItems = extractCriticalContext(summary);
|
|
5061
|
-
const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
|
|
5062
|
-
const prevState = loadCompactionState(rc.projectId);
|
|
5063
|
-
if (prevState) {
|
|
5064
|
-
const delta = computeDelta(prevState, compactionState);
|
|
5065
|
-
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
5066
|
-
summary = injectDeltaSection(summary, delta);
|
|
5067
|
-
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
5071
|
-
const detRead = extraction.readFiles;
|
|
5072
|
-
const estimatedAfter = estimateTokens(summary) + rc.accTokens;
|
|
5073
|
-
const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
|
|
5074
|
-
const details = {
|
|
5075
|
-
method: rc.method,
|
|
5076
|
-
chunkCount: rc.chunkCount || 1,
|
|
5077
|
-
topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
|
|
5078
|
-
readFiles: detRead,
|
|
5079
|
-
modifiedFiles: detModified,
|
|
5080
|
-
totalMessages: rc.toCompact.length,
|
|
5081
|
-
totalTokensSummarized: rc.convTokens,
|
|
5082
|
-
llmCalls: rc.llmCalls,
|
|
5083
|
-
profile: rc.profile,
|
|
5084
|
-
backupPath: rc.backupPath,
|
|
5085
|
-
tokensSaved,
|
|
5086
|
-
verified: rc.verified,
|
|
5087
|
-
gaps: rc.verificationGaps,
|
|
5088
|
-
explorationRounds: rc.explorationRounds,
|
|
5089
|
-
explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
|
|
5090
|
-
model: rc.modelLabel,
|
|
5091
|
-
qualityScore: rc.verificationScore,
|
|
5092
|
-
tokensBefore: rc.totalTokens,
|
|
5093
|
-
compactionState,
|
|
5094
|
-
openLoops
|
|
5095
|
-
};
|
|
5096
|
-
const out = rc;
|
|
5097
|
-
out.finalSummary = summary;
|
|
5098
|
-
out.openLoops = openLoops;
|
|
5099
|
-
out.compactionState = compactionState;
|
|
5100
|
-
out.details = details;
|
|
5101
|
-
out.tokensSaved = tokensSaved;
|
|
5102
|
-
return advance(out, "_stated");
|
|
5103
|
-
}
|
|
5104
|
-
|
|
5105
5311
|
// src/utils/damage.ts
|
|
5106
5312
|
init_logger();
|
|
5107
5313
|
init_paths();
|
|
@@ -5113,6 +5319,7 @@ var COMPLAINT_PATTERNS = [
|
|
|
5113
5319
|
];
|
|
5114
5320
|
function detectDamage(postMessages, details) {
|
|
5115
5321
|
const signals = [];
|
|
5322
|
+
const reReadFiles = [];
|
|
5116
5323
|
const compactedFiles = new Set(details.modifiedFiles.map((f) => f.toLowerCase()));
|
|
5117
5324
|
const compactedReadFiles = new Set(details.readFiles.map((f) => f.toLowerCase()));
|
|
5118
5325
|
for (let i = 0;i < postMessages.length; i++) {
|
|
@@ -5131,6 +5338,8 @@ function detectDamage(postMessages, details) {
|
|
|
5131
5338
|
severity: "medium",
|
|
5132
5339
|
detail: "Agent re-read compacted file: " + fp
|
|
5133
5340
|
});
|
|
5341
|
+
if (!reReadFiles.includes(fp))
|
|
5342
|
+
reReadFiles.push(fp);
|
|
5134
5343
|
}
|
|
5135
5344
|
}
|
|
5136
5345
|
}
|
|
@@ -5182,7 +5391,8 @@ function detectDamage(postMessages, details) {
|
|
|
5182
5391
|
return {
|
|
5183
5392
|
signals,
|
|
5184
5393
|
damageScore,
|
|
5185
|
-
summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)"
|
|
5394
|
+
summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)",
|
|
5395
|
+
reReadFiles
|
|
5186
5396
|
};
|
|
5187
5397
|
}
|
|
5188
5398
|
function logDamageReport(sessionId, report, details) {
|
|
@@ -5202,6 +5412,92 @@ function logDamageReport(sessionId, report, details) {
|
|
|
5202
5412
|
warn("logDamageReport failed", e);
|
|
5203
5413
|
}
|
|
5204
5414
|
}
|
|
5415
|
+
var REMEDIATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5416
|
+
function writeRemediationHints(projectId, files) {
|
|
5417
|
+
if (!files.length)
|
|
5418
|
+
return;
|
|
5419
|
+
const cleaned = [...new Set(files.map((f) => (f ?? "").trim()).filter((f) => f.length > 0))];
|
|
5420
|
+
if (!cleaned.length)
|
|
5421
|
+
return;
|
|
5422
|
+
try {
|
|
5423
|
+
writeJsonSync(remediationHintsFile(projectId), { files: cleaned, updatedAt: Date.now() });
|
|
5424
|
+
} catch (e) {
|
|
5425
|
+
warn("writeRemediationHints failed", e);
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
function readRemediationHints(projectId) {
|
|
5429
|
+
const data = readJsonSync(remediationHintsFile(projectId));
|
|
5430
|
+
if (!data || !Array.isArray(data.files))
|
|
5431
|
+
return [];
|
|
5432
|
+
if (typeof data.updatedAt === "number" && Date.now() - data.updatedAt > REMEDIATION_TTL_MS)
|
|
5433
|
+
return [];
|
|
5434
|
+
return data.files.filter((f) => typeof f === "string");
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
// src/app/steps/state.ts
|
|
5438
|
+
init_tokens();
|
|
5439
|
+
function buildState(rc) {
|
|
5440
|
+
const extraction = rc.extraction;
|
|
5441
|
+
let summary = rc.finalSummary;
|
|
5442
|
+
const openLoops = extractOpenLoops(rc.llmMessages, extraction);
|
|
5443
|
+
if (openLoops.length > 0) {
|
|
5444
|
+
rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
5445
|
+
summary = injectOpenLoopsSection(summary, openLoops);
|
|
5446
|
+
}
|
|
5447
|
+
const pinPaths = rc.config.pinPaths ?? [];
|
|
5448
|
+
const remediated = readRemediationHints(rc.projectId);
|
|
5449
|
+
const preserve = remediated.length ? Array.from(new Set([...pinPaths, ...remediated])) : pinPaths;
|
|
5450
|
+
if (remediated.length) {
|
|
5451
|
+
rc.notify("Remediation: re-preserving " + remediated.length + " file(s) lost in a prior compaction", "info");
|
|
5452
|
+
}
|
|
5453
|
+
if (preserve.length > 0) {
|
|
5454
|
+
summary = ensurePinnedPaths(summary, preserve);
|
|
5455
|
+
}
|
|
5456
|
+
const nextActions = extractNextActions(summary);
|
|
5457
|
+
const criticalContextItems = extractCriticalContext(summary);
|
|
5458
|
+
const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
|
|
5459
|
+
const prevState = loadCompactionState(rc.projectId);
|
|
5460
|
+
if (prevState) {
|
|
5461
|
+
const delta = computeDelta(prevState, compactionState);
|
|
5462
|
+
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
5463
|
+
summary = injectDeltaSection(summary, delta);
|
|
5464
|
+
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
5468
|
+
const detRead = extraction.readFiles;
|
|
5469
|
+
const estimatedAfter = estimateTokens(summary) + rc.accTokens;
|
|
5470
|
+
const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
|
|
5471
|
+
const details = {
|
|
5472
|
+
method: rc.method,
|
|
5473
|
+
chunkCount: rc.chunkCount || 1,
|
|
5474
|
+
topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
|
|
5475
|
+
readFiles: detRead,
|
|
5476
|
+
modifiedFiles: detModified,
|
|
5477
|
+
totalMessages: rc.toCompact.length,
|
|
5478
|
+
totalTokensSummarized: rc.convTokens,
|
|
5479
|
+
llmCalls: rc.llmCalls,
|
|
5480
|
+
profile: rc.profile,
|
|
5481
|
+
backupPath: rc.backupPath,
|
|
5482
|
+
tokensSaved,
|
|
5483
|
+
verified: rc.verified,
|
|
5484
|
+
gaps: rc.verificationGaps,
|
|
5485
|
+
explorationRounds: rc.explorationRounds,
|
|
5486
|
+
explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
|
|
5487
|
+
model: rc.modelLabel,
|
|
5488
|
+
qualityScore: rc.verificationScore,
|
|
5489
|
+
tokensBefore: rc.totalTokens,
|
|
5490
|
+
compactionState,
|
|
5491
|
+
openLoops
|
|
5492
|
+
};
|
|
5493
|
+
const out = rc;
|
|
5494
|
+
out.finalSummary = summary;
|
|
5495
|
+
out.openLoops = openLoops;
|
|
5496
|
+
out.compactionState = compactionState;
|
|
5497
|
+
out.details = details;
|
|
5498
|
+
out.tokensSaved = tokensSaved;
|
|
5499
|
+
return advance(out, "_stated");
|
|
5500
|
+
}
|
|
5205
5501
|
|
|
5206
5502
|
// src/app/steps/persist.ts
|
|
5207
5503
|
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -5212,7 +5508,7 @@ function persistDurableState(rc) {
|
|
|
5212
5508
|
}
|
|
5213
5509
|
function runDamageDetection(rc) {
|
|
5214
5510
|
try {
|
|
5215
|
-
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([e.message])).flat();
|
|
5511
|
+
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([asBranchMessage(e.message)])).flat();
|
|
5216
5512
|
if (postCompactMsgs.length <= 2)
|
|
5217
5513
|
return;
|
|
5218
5514
|
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
@@ -5228,6 +5524,9 @@ function runDamageDetection(rc) {
|
|
|
5228
5524
|
rc.notify("Previous compaction damage: " + damage.summary, "warning");
|
|
5229
5525
|
}
|
|
5230
5526
|
logDamageReport(rc.sessionId, damage, safeDetails);
|
|
5527
|
+
if (damage.reReadFiles.length > 0) {
|
|
5528
|
+
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
5529
|
+
}
|
|
5231
5530
|
} catch (err) {
|
|
5232
5531
|
warn("Damage detection error", err);
|
|
5233
5532
|
}
|
|
@@ -5437,8 +5736,16 @@ async function runSmartCompact(opts) {
|
|
|
5437
5736
|
recordSuccessMetrics(stated, "success");
|
|
5438
5737
|
if (!stated.flags.autoTriggered) {
|
|
5439
5738
|
try {
|
|
5440
|
-
|
|
5441
|
-
|
|
5739
|
+
let resultTimer;
|
|
5740
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
5741
|
+
resultTimer = setTimeout(resolve, 5000);
|
|
5742
|
+
});
|
|
5743
|
+
try {
|
|
5744
|
+
await Promise.race([showResultScreen(stated.ctx, stated.details, stated.extraction, stated.services), timeoutPromise]);
|
|
5745
|
+
} finally {
|
|
5746
|
+
if (resultTimer)
|
|
5747
|
+
clearTimeout(resultTimer);
|
|
5748
|
+
}
|
|
5442
5749
|
} catch (err) {
|
|
5443
5750
|
warn("Result screen error", err);
|
|
5444
5751
|
stated.notify("Result screen skipped", "info");
|
|
@@ -5578,9 +5885,9 @@ function smartCompactExtension(pi) {
|
|
|
5578
5885
|
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
5579
5886
|
const isRunning = { value: false };
|
|
5580
5887
|
pi.registerCommand("smart-compact", {
|
|
5581
|
-
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
|
|
5888
|
+
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run|restore|metrics|dashboard] [note]",
|
|
5582
5889
|
getArgumentCompletions: (prefix) => {
|
|
5583
|
-
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
5890
|
+
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "restore", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
5584
5891
|
return m.length ? m : null;
|
|
5585
5892
|
},
|
|
5586
5893
|
handler: async (args, ctx) => {
|
|
@@ -5609,6 +5916,48 @@ function smartCompactExtension(pi) {
|
|
|
5609
5916
|
}
|
|
5610
5917
|
return;
|
|
5611
5918
|
}
|
|
5919
|
+
if (flags.includes("restore")) {
|
|
5920
|
+
const backups = listBackups();
|
|
5921
|
+
if (!backups.length) {
|
|
5922
|
+
ctx.ui.notify("No smart-compact backups found", "info");
|
|
5923
|
+
return;
|
|
5924
|
+
}
|
|
5925
|
+
const selected = await showRestorePicker(ctx, backups);
|
|
5926
|
+
if (!selected) {
|
|
5927
|
+
ctx.ui.notify("Cancelled", "info");
|
|
5928
|
+
return;
|
|
5929
|
+
}
|
|
5930
|
+
const content = readBackupContent(selected);
|
|
5931
|
+
if (!content) {
|
|
5932
|
+
ctx.ui.notify("Could not read backup: " + selected, "error");
|
|
5933
|
+
return;
|
|
5934
|
+
}
|
|
5935
|
+
const action = await showRestoreAction(ctx, selected);
|
|
5936
|
+
if (action === "restore") {
|
|
5937
|
+
const branch = ctx.sessionManager.getBranch();
|
|
5938
|
+
const leafId = branch.length ? branch[branch.length - 1].id : undefined;
|
|
5939
|
+
if (!leafId) {
|
|
5940
|
+
ctx.ui.notify("Cannot restore: no session leaf to fork from \u2014 showing content instead", "warning");
|
|
5941
|
+
await showBackupViewer(ctx, content, selected);
|
|
5942
|
+
return;
|
|
5943
|
+
}
|
|
5944
|
+
try {
|
|
5945
|
+
const result = await ctx.fork(leafId, {
|
|
5946
|
+
withSession: async (rctx) => {
|
|
5947
|
+
await rctx.sendMessage(buildRestoreMessage(content, selected), { deliverAs: "nextTurn" });
|
|
5948
|
+
}
|
|
5949
|
+
});
|
|
5950
|
+
ctx.ui.notify(result.cancelled ? "Restore cancelled" : "Restored backup into a new session", "info");
|
|
5951
|
+
} catch (e) {
|
|
5952
|
+
warn("Restore fork failed", e);
|
|
5953
|
+
ctx.ui.notify("Restore failed (" + (e instanceof Error ? e.message : String(e)) + ") \u2014 showing content instead", "warning");
|
|
5954
|
+
await showBackupViewer(ctx, content, selected);
|
|
5955
|
+
}
|
|
5956
|
+
} else if (action === "view") {
|
|
5957
|
+
await showBackupViewer(ctx, content, selected);
|
|
5958
|
+
}
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5612
5961
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
5613
5962
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
5614
5963
|
const profile = profileArg ?? loadConfig().profile;
|