pi-smart-compact 7.15.1 → 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 +33 -0
- package/README.md +166 -285
- 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/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +443 -118
- 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 +7 -0
- 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 +1 -1
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)
|
|
@@ -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;
|
|
@@ -2947,6 +3139,14 @@ function resolveCompactionWindow(rc) {
|
|
|
2947
3139
|
// src/app/steps/recover.ts
|
|
2948
3140
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2949
3141
|
|
|
3142
|
+
// src/infra/ai-messages.ts
|
|
3143
|
+
function asBranchMessage(message) {
|
|
3144
|
+
return message;
|
|
3145
|
+
}
|
|
3146
|
+
function asSerializableMessages(msgs) {
|
|
3147
|
+
return msgs;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
2950
3150
|
// src/utils/session-log.ts
|
|
2951
3151
|
import * as fs4 from "fs";
|
|
2952
3152
|
import * as path7 from "path";
|
|
@@ -3059,17 +3259,19 @@ function findSessionLogFile(sessionId) {
|
|
|
3059
3259
|
}
|
|
3060
3260
|
return remember(null);
|
|
3061
3261
|
}
|
|
3062
|
-
function normalizeLogMessage(msg) {
|
|
3262
|
+
function normalizeLogMessage(msg, entryTimestamp) {
|
|
3063
3263
|
if (!msg || !msg.role)
|
|
3064
3264
|
return null;
|
|
3065
3265
|
const role = msg.role;
|
|
3066
3266
|
if (role === "user" || role === "assistant" || role === "toolResult") {
|
|
3267
|
+
const ts = entryTimestamp ? Date.parse(entryTimestamp) : NaN;
|
|
3067
3268
|
return {
|
|
3068
3269
|
role,
|
|
3069
3270
|
content: msg.content,
|
|
3070
3271
|
isError: msg.isError,
|
|
3071
3272
|
toolCallId: msg.toolCallId,
|
|
3072
|
-
|
|
3273
|
+
toolName: msg.toolName,
|
|
3274
|
+
timestamp: Number.isFinite(ts) ? ts : undefined
|
|
3073
3275
|
};
|
|
3074
3276
|
}
|
|
3075
3277
|
return null;
|
|
@@ -3100,7 +3302,7 @@ function readOriginalMessageMap(sessionId) {
|
|
|
3100
3302
|
continue;
|
|
3101
3303
|
}
|
|
3102
3304
|
if (entry.type === "message" && entry.id && entry.message) {
|
|
3103
|
-
const normalized = normalizeLogMessage(entry.message);
|
|
3305
|
+
const normalized = normalizeLogMessage(entry.message, entry.timestamp);
|
|
3104
3306
|
if (normalized)
|
|
3105
3307
|
map.set(entry.id, normalized);
|
|
3106
3308
|
}
|
|
@@ -3148,7 +3350,7 @@ function resolveCompactionMessages(sessionId, toCompactEntries) {
|
|
|
3148
3350
|
|
|
3149
3351
|
// src/app/steps/recover.ts
|
|
3150
3352
|
function recoverSessionLog(rc) {
|
|
3151
|
-
let llmMessages = convertToLlm(rc.toCompact.map((e) => e.message));
|
|
3353
|
+
let llmMessages = convertToLlm(rc.toCompact.map((e) => asBranchMessage(e.message)));
|
|
3152
3354
|
if (hasTruncatedMessages(llmMessages)) {
|
|
3153
3355
|
const fromLog = resolveCompactionMessages(rc.sessionId, rc.toCompact);
|
|
3154
3356
|
if (fromLog) {
|
|
@@ -3551,7 +3753,7 @@ function extractWithCache(rc) {
|
|
|
3551
3753
|
const pruneEnd = Date.now();
|
|
3552
3754
|
markMeasuredPhase(rc, "prune", extractStepStart, pruneEnd);
|
|
3553
3755
|
const extractionStart = pruneEnd;
|
|
3554
|
-
const convText = serializeConversation(rc.llmMessages);
|
|
3756
|
+
const convText = serializeConversation(asSerializableMessages(rc.llmMessages));
|
|
3555
3757
|
const convTokens = estimateTokens(convText);
|
|
3556
3758
|
const backupPath = backupConversation(convText, rc.sessionId);
|
|
3557
3759
|
const prevContext = getPreviousCompactionContext(rc.branch);
|
|
@@ -3622,6 +3824,7 @@ function extractWithCache(rc) {
|
|
|
3622
3824
|
init_overlays();
|
|
3623
3825
|
|
|
3624
3826
|
// src/phases/explore.ts
|
|
3827
|
+
import { Type } from "typebox";
|
|
3625
3828
|
init_constants();
|
|
3626
3829
|
init_cache();
|
|
3627
3830
|
init_tokens();
|
|
@@ -3644,32 +3847,32 @@ var EXPLORATION_TOOLS = [
|
|
|
3644
3847
|
{
|
|
3645
3848
|
name: "get_message_range",
|
|
3646
3849
|
description: "Get compact summaries of messages from start to end index (0-based).",
|
|
3647
|
-
parameters: {
|
|
3850
|
+
parameters: Type.Object({ start: Type.Number(), end: Type.Number() })
|
|
3648
3851
|
},
|
|
3649
3852
|
{
|
|
3650
3853
|
name: "search_conversation",
|
|
3651
3854
|
description: "Search for text in conversation messages.",
|
|
3652
|
-
parameters: {
|
|
3855
|
+
parameters: Type.Object({ query: Type.String() })
|
|
3653
3856
|
},
|
|
3654
3857
|
{
|
|
3655
3858
|
name: "get_recent_user_messages",
|
|
3656
3859
|
description: "Get the last N user messages.",
|
|
3657
|
-
parameters: {
|
|
3860
|
+
parameters: Type.Object({ count: Type.Optional(Type.Number()) })
|
|
3658
3861
|
},
|
|
3659
3862
|
{
|
|
3660
3863
|
name: "get_context_around",
|
|
3661
3864
|
description: "Get context around a specific message index.",
|
|
3662
|
-
parameters: {
|
|
3865
|
+
parameters: Type.Object({ index: Type.Number(), radius: Type.Optional(Type.Number()) })
|
|
3663
3866
|
},
|
|
3664
3867
|
{
|
|
3665
3868
|
name: "get_file_changes",
|
|
3666
3869
|
description: "Get tool calls that modified a specific file.",
|
|
3667
|
-
parameters: {
|
|
3870
|
+
parameters: Type.Object({ path: Type.String() })
|
|
3668
3871
|
},
|
|
3669
3872
|
{
|
|
3670
3873
|
name: "get_error_chain",
|
|
3671
3874
|
description: "Get all messages related to a specific error.",
|
|
3672
|
-
parameters: {
|
|
3875
|
+
parameters: Type.Object({ index: Type.Number(), context_radius: Type.Optional(Type.Number()) })
|
|
3673
3876
|
}
|
|
3674
3877
|
];
|
|
3675
3878
|
function executeExplorationTool(call, llmMessages) {
|
|
@@ -3785,40 +3988,53 @@ function parseExplorationReport(text, llmMessages) {
|
|
|
3785
3988
|
if (boundaryMatch) {
|
|
3786
3989
|
try {
|
|
3787
3990
|
const boundaries = JSON.parse("[" + boundaryMatch[1] + "]");
|
|
3788
|
-
return { ...fallbackExplorationReport(llmMessages), boundaries: boundaries
|
|
3789
|
-
afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
|
|
3790
|
-
topic: String(b.topic ?? "").slice(0, 100),
|
|
3791
|
-
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
3792
|
-
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
3793
|
-
})) };
|
|
3991
|
+
return { ...fallbackExplorationReport(llmMessages), boundaries: normalizeBoundaries(boundaries, llmMessages.length) };
|
|
3794
3992
|
} catch (err) {
|
|
3795
3993
|
debug("Boundary JSON parse failed", err);
|
|
3796
3994
|
}
|
|
3797
3995
|
}
|
|
3798
3996
|
return fallbackExplorationReport(llmMessages);
|
|
3799
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
|
+
}
|
|
3800
4018
|
function buildExplorationReportFromParsed(parsed, llmMessages) {
|
|
3801
4019
|
if (typeof parsed !== "object" || parsed === null) {
|
|
3802
4020
|
return fallbackExplorationReport(llmMessages);
|
|
3803
4021
|
}
|
|
4022
|
+
const p = parsed;
|
|
4023
|
+
const statusAssessment = p.statusAssessment ?? null;
|
|
4024
|
+
const sessionTypeRaw = p.sessionType;
|
|
3804
4025
|
return {
|
|
3805
|
-
boundaries: (
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
})),
|
|
3811
|
-
mainGoal: parsed.mainGoal ?? "",
|
|
3812
|
-
sessionType: ["implementation", "review", "debugging", "discussion"].includes(parsed.sessionType) ? parsed.sessionType : "implementation",
|
|
3813
|
-
enrichedConstraints: Array.isArray(parsed.enrichedConstraints) ? parsed.enrichedConstraints.map(String) : [],
|
|
3814
|
-
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),
|
|
3815
4031
|
statusAssessment: {
|
|
3816
|
-
done:
|
|
3817
|
-
inProgress:
|
|
3818
|
-
blocked:
|
|
4032
|
+
done: stringArray(statusAssessment?.done),
|
|
4033
|
+
inProgress: stringArray(statusAssessment?.inProgress),
|
|
4034
|
+
blocked: stringArray(statusAssessment?.blocked)
|
|
3819
4035
|
},
|
|
3820
|
-
criticalContext:
|
|
3821
|
-
keyDecisions:
|
|
4036
|
+
criticalContext: stringArray(p.criticalContext),
|
|
4037
|
+
keyDecisions: stringArray(p.keyDecisions)
|
|
3822
4038
|
};
|
|
3823
4039
|
}
|
|
3824
4040
|
function fallbackExplorationReport(llmMessages) {
|
|
@@ -3886,7 +4102,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3886
4102
|
toolSupport.set(cacheKey, true, svc.clock.now());
|
|
3887
4103
|
const messages = [
|
|
3888
4104
|
{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() },
|
|
3889
|
-
|
|
4105
|
+
probeResp
|
|
3890
4106
|
];
|
|
3891
4107
|
for (const tc of toolCalls) {
|
|
3892
4108
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
@@ -3920,7 +4136,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
3920
4136
|
}
|
|
3921
4137
|
return { report: report2, rounds, toolSupported: true };
|
|
3922
4138
|
}
|
|
3923
|
-
messages.push(
|
|
4139
|
+
messages.push(response);
|
|
3924
4140
|
for (const tc of nextToolCalls) {
|
|
3925
4141
|
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
3926
4142
|
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
@@ -4236,6 +4452,19 @@ function assembleFallback(summaries, extraction) {
|
|
|
4236
4452
|
].join(`
|
|
4237
4453
|
`);
|
|
4238
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
|
+
}
|
|
4239
4468
|
|
|
4240
4469
|
// src/app/steps/synthesize.ts
|
|
4241
4470
|
init_constants();
|
|
@@ -4354,10 +4583,15 @@ async function summarizeConversation(rc) {
|
|
|
4354
4583
|
}
|
|
4355
4584
|
const concurrency = rc.providerCaps.concurrencyLimit;
|
|
4356
4585
|
if (totalBatches <= 1) {
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
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");
|
|
4361
4595
|
}
|
|
4362
4596
|
} else {
|
|
4363
4597
|
const results = new Array(totalBatches);
|
|
@@ -4434,19 +4668,6 @@ async function summarizeConversation(rc) {
|
|
|
4434
4668
|
markMeasuredPhase(out, "synthesize", synthPhaseStart);
|
|
4435
4669
|
return advance(out, "_synthesized");
|
|
4436
4670
|
}
|
|
4437
|
-
function failedChunkSummary(ch) {
|
|
4438
|
-
return {
|
|
4439
|
-
topic: ch.topic,
|
|
4440
|
-
startIndex: ch.startIndex,
|
|
4441
|
-
endIndex: ch.endIndex,
|
|
4442
|
-
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
4443
|
-
`).slice(0, 300),
|
|
4444
|
-
keyDecisions: [],
|
|
4445
|
-
filesModified: [],
|
|
4446
|
-
filesRead: [],
|
|
4447
|
-
priority: ch.priority
|
|
4448
|
-
};
|
|
4449
|
-
}
|
|
4450
4671
|
|
|
4451
4672
|
// src/phases/verify.ts
|
|
4452
4673
|
init_constants();
|
|
@@ -4918,6 +5139,10 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4918
5139
|
let decisionId = 0;
|
|
4919
5140
|
let constraintId = 0;
|
|
4920
5141
|
let errorId = 0;
|
|
5142
|
+
const modifiedBasenames = extraction.modifiedFiles.map((f) => ({
|
|
5143
|
+
path: f.path,
|
|
5144
|
+
bn: f.path.split("/").pop()?.toLowerCase() ?? ""
|
|
5145
|
+
}));
|
|
4921
5146
|
return {
|
|
4922
5147
|
goal: extraction.mainGoal,
|
|
4923
5148
|
decisions: extraction.decisions.map((d) => ({
|
|
@@ -4936,7 +5161,8 @@ function buildCompactionState(extraction, openLoops, report, nextActions, critic
|
|
|
4936
5161
|
readFiles: extraction.readFiles,
|
|
4937
5162
|
deletedFiles: extraction.deletedFiles,
|
|
4938
5163
|
unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
|
|
4939
|
-
const
|
|
5164
|
+
const msgLower = e.message.toLowerCase();
|
|
5165
|
+
const bn = modifiedBasenames.find((f) => f.bn.length > 0 && msgLower.includes(f.bn));
|
|
4940
5166
|
return {
|
|
4941
5167
|
id: "error-" + ++errorId,
|
|
4942
5168
|
message: e.message.slice(0, 300),
|
|
@@ -5055,6 +5281,18 @@ function injectDeltaSection(summary, delta) {
|
|
|
5055
5281
|
const updated = upsertSection(parsed, "changes", body, placement);
|
|
5056
5282
|
return renderSummary(updated);
|
|
5057
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):");
|
|
5294
|
+
return renderSummary(updated);
|
|
5295
|
+
}
|
|
5058
5296
|
function extractNextActions(summary) {
|
|
5059
5297
|
const match = summary.match(/## Next Steps\s*\n([\s\S]*?)(?=##|$)/);
|
|
5060
5298
|
if (!match)
|
|
@@ -5070,62 +5308,6 @@ function extractCriticalContext(summary) {
|
|
|
5070
5308
|
`).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5071
5309
|
}
|
|
5072
5310
|
|
|
5073
|
-
// src/app/steps/state.ts
|
|
5074
|
-
init_tokens();
|
|
5075
|
-
function buildState(rc) {
|
|
5076
|
-
const extraction = rc.extraction;
|
|
5077
|
-
let summary = rc.finalSummary;
|
|
5078
|
-
const openLoops = extractOpenLoops(rc.llmMessages, extraction);
|
|
5079
|
-
if (openLoops.length > 0) {
|
|
5080
|
-
rc.notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
5081
|
-
summary = injectOpenLoopsSection(summary, openLoops);
|
|
5082
|
-
}
|
|
5083
|
-
const nextActions = extractNextActions(summary);
|
|
5084
|
-
const criticalContextItems = extractCriticalContext(summary);
|
|
5085
|
-
const compactionState = buildCompactionState(extraction, openLoops, rc.explorationReport, nextActions, criticalContextItems);
|
|
5086
|
-
const prevState = loadCompactionState(rc.projectId);
|
|
5087
|
-
if (prevState) {
|
|
5088
|
-
const delta = computeDelta(prevState, compactionState);
|
|
5089
|
-
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
5090
|
-
summary = injectDeltaSection(summary, delta);
|
|
5091
|
-
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
5095
|
-
const detRead = extraction.readFiles;
|
|
5096
|
-
const estimatedAfter = estimateTokens(summary) + rc.accTokens;
|
|
5097
|
-
const tokensSaved = Math.max(0, rc.totalTokens - estimatedAfter);
|
|
5098
|
-
const details = {
|
|
5099
|
-
method: rc.method,
|
|
5100
|
-
chunkCount: rc.chunkCount || 1,
|
|
5101
|
-
topics: rc.summaries.length ? rc.summaries.map((s) => s.topic) : [rc.method],
|
|
5102
|
-
readFiles: detRead,
|
|
5103
|
-
modifiedFiles: detModified,
|
|
5104
|
-
totalMessages: rc.toCompact.length,
|
|
5105
|
-
totalTokensSummarized: rc.convTokens,
|
|
5106
|
-
llmCalls: rc.llmCalls,
|
|
5107
|
-
profile: rc.profile,
|
|
5108
|
-
backupPath: rc.backupPath,
|
|
5109
|
-
tokensSaved,
|
|
5110
|
-
verified: rc.verified,
|
|
5111
|
-
gaps: rc.verificationGaps,
|
|
5112
|
-
explorationRounds: rc.explorationRounds,
|
|
5113
|
-
explorationBoundaries: rc.explorationReport?.boundaries.length ?? 0,
|
|
5114
|
-
model: rc.modelLabel,
|
|
5115
|
-
qualityScore: rc.verificationScore,
|
|
5116
|
-
tokensBefore: rc.totalTokens,
|
|
5117
|
-
compactionState,
|
|
5118
|
-
openLoops
|
|
5119
|
-
};
|
|
5120
|
-
const out = rc;
|
|
5121
|
-
out.finalSummary = summary;
|
|
5122
|
-
out.openLoops = openLoops;
|
|
5123
|
-
out.compactionState = compactionState;
|
|
5124
|
-
out.details = details;
|
|
5125
|
-
out.tokensSaved = tokensSaved;
|
|
5126
|
-
return advance(out, "_stated");
|
|
5127
|
-
}
|
|
5128
|
-
|
|
5129
5311
|
// src/utils/damage.ts
|
|
5130
5312
|
init_logger();
|
|
5131
5313
|
init_paths();
|
|
@@ -5137,6 +5319,7 @@ var COMPLAINT_PATTERNS = [
|
|
|
5137
5319
|
];
|
|
5138
5320
|
function detectDamage(postMessages, details) {
|
|
5139
5321
|
const signals = [];
|
|
5322
|
+
const reReadFiles = [];
|
|
5140
5323
|
const compactedFiles = new Set(details.modifiedFiles.map((f) => f.toLowerCase()));
|
|
5141
5324
|
const compactedReadFiles = new Set(details.readFiles.map((f) => f.toLowerCase()));
|
|
5142
5325
|
for (let i = 0;i < postMessages.length; i++) {
|
|
@@ -5155,6 +5338,8 @@ function detectDamage(postMessages, details) {
|
|
|
5155
5338
|
severity: "medium",
|
|
5156
5339
|
detail: "Agent re-read compacted file: " + fp
|
|
5157
5340
|
});
|
|
5341
|
+
if (!reReadFiles.includes(fp))
|
|
5342
|
+
reReadFiles.push(fp);
|
|
5158
5343
|
}
|
|
5159
5344
|
}
|
|
5160
5345
|
}
|
|
@@ -5206,7 +5391,8 @@ function detectDamage(postMessages, details) {
|
|
|
5206
5391
|
return {
|
|
5207
5392
|
signals,
|
|
5208
5393
|
damageScore,
|
|
5209
|
-
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
|
|
5210
5396
|
};
|
|
5211
5397
|
}
|
|
5212
5398
|
function logDamageReport(sessionId, report, details) {
|
|
@@ -5226,6 +5412,92 @@ function logDamageReport(sessionId, report, details) {
|
|
|
5226
5412
|
warn("logDamageReport failed", e);
|
|
5227
5413
|
}
|
|
5228
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
|
+
}
|
|
5229
5501
|
|
|
5230
5502
|
// src/app/steps/persist.ts
|
|
5231
5503
|
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -5236,7 +5508,7 @@ function persistDurableState(rc) {
|
|
|
5236
5508
|
}
|
|
5237
5509
|
function runDamageDetection(rc) {
|
|
5238
5510
|
try {
|
|
5239
|
-
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();
|
|
5240
5512
|
if (postCompactMsgs.length <= 2)
|
|
5241
5513
|
return;
|
|
5242
5514
|
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
@@ -5252,6 +5524,9 @@ function runDamageDetection(rc) {
|
|
|
5252
5524
|
rc.notify("Previous compaction damage: " + damage.summary, "warning");
|
|
5253
5525
|
}
|
|
5254
5526
|
logDamageReport(rc.sessionId, damage, safeDetails);
|
|
5527
|
+
if (damage.reReadFiles.length > 0) {
|
|
5528
|
+
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
5529
|
+
}
|
|
5255
5530
|
} catch (err) {
|
|
5256
5531
|
warn("Damage detection error", err);
|
|
5257
5532
|
}
|
|
@@ -5461,8 +5736,16 @@ async function runSmartCompact(opts) {
|
|
|
5461
5736
|
recordSuccessMetrics(stated, "success");
|
|
5462
5737
|
if (!stated.flags.autoTriggered) {
|
|
5463
5738
|
try {
|
|
5464
|
-
|
|
5465
|
-
|
|
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
|
+
}
|
|
5466
5749
|
} catch (err) {
|
|
5467
5750
|
warn("Result screen error", err);
|
|
5468
5751
|
stated.notify("Result screen skipped", "info");
|
|
@@ -5602,9 +5885,9 @@ function smartCompactExtension(pi) {
|
|
|
5602
5885
|
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
5603
5886
|
const isRunning = { value: false };
|
|
5604
5887
|
pi.registerCommand("smart-compact", {
|
|
5605
|
-
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]",
|
|
5606
5889
|
getArgumentCompletions: (prefix) => {
|
|
5607
|
-
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 }));
|
|
5608
5891
|
return m.length ? m : null;
|
|
5609
5892
|
},
|
|
5610
5893
|
handler: async (args, ctx) => {
|
|
@@ -5633,6 +5916,48 @@ function smartCompactExtension(pi) {
|
|
|
5633
5916
|
}
|
|
5634
5917
|
return;
|
|
5635
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
|
+
}
|
|
5636
5961
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
5637
5962
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
5638
5963
|
const profile = profileArg ?? loadConfig().profile;
|