fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)

This commit is contained in:
mac
2026-08-16 12:45:32 +08:00
parent e9a261482c
commit 1b687c01e9
50 changed files with 10885 additions and 1161 deletions
+162 -8
View File
@@ -32,6 +32,7 @@ const seenRollouts = new Set(); // 已见过的 rollout 文件(判定「新会
const queue = [];
let working = false;
let cfg = null;
const deletedSessionIds = new Set();
// ---------------------------------------------------------------------------
// 配置(userData/memory.json)
@@ -123,6 +124,33 @@ function flushDb() {
log("会话记忆:记忆库写盘失败 " + (e.message || e));
}
}
function flushDbStrict() {
clearTimeout(persistTimer);
if (!db) return;
const file = dbFile();
const tmp = file + ".td-delete-" + process.pid + ".tmp";
const old = file + ".td-delete-" + process.pid + ".old";
fs.rmSync(old, { force: true });
const fd = fs.openSync(tmp, "w");
try {
fs.writeFileSync(fd, Buffer.from(db.export()));
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
try {
if (fs.existsSync(file)) fs.renameSync(file, old);
try {
fs.renameSync(tmp, file);
} catch (e) {
if (fs.existsSync(old)) fs.renameSync(old, file);
throw e;
}
try { fs.rmSync(old, { force: true }); } catch {}
} finally {
fs.rmSync(tmp, { force: true });
}
}
function all(sql, params = []) {
const rows = [];
const st = db.prepare(sql);
@@ -270,6 +298,7 @@ async function summarizeFile(file) {
const items = sessions.listConversations({ includeSubagent: false });
const item = items.find((i) => i.path === file);
if (!item || !item.id) return; // 子 agent 线程或读不到元信息 → 跳过
if (deletedSessionIds.has(item.id)) return;
const conv = sessions.readConversation(file);
if (!conv.messages.some((m) => m.role === "user")) return; // 空会话不摘要
let r;
@@ -280,6 +309,7 @@ async function summarizeFile(file) {
r = localSummarize(item, conv);
}
await ensureDb();
if (deletedSessionIds.has(item.id)) return;
db.run(
`INSERT INTO sessions (id, ts, project, cwd, topic, summary, decisions, next_steps, files,
rollout_path, rollout_size, engine, updated_at)
@@ -618,6 +648,65 @@ function writeProjectCore(cwd) {
if (nt !== text) writeFileAtomic(f, nt);
}
function removeProjectCore(cwd) {
const f = path.join(cwd, "AGENTS.md");
let text = "";
try { text = fs.readFileSync(f, "utf-8"); } catch { return; }
const si = text.indexOf(AGENTS_START);
const ei = text.indexOf(AGENTS_END);
if (si < 0 || ei <= si) return;
const next = text.slice(0, si) + text.slice(ei + AGENTS_END.length);
writeFileAtomic(f, next);
}
function removeArchiveSession(cwd, id) {
if (!cwd || !id) return;
const f = archivePath(cwd);
let text = "";
try { text = fs.readFileSync(f, "utf-8"); } catch { return; }
const lines = text.split("\n");
const kept = [];
let dropping = false;
for (const line of lines) {
const header = line.match(/^## .* \[([0-9a-f-]{36})\]\s*$/i);
if (header) dropping = header[1] === id;
if (!dropping) kept.push(line);
}
const next = kept.join("\n");
if (next.trim()) writeFileAtomic(f, next);
else fs.rmSync(f, { force: true });
}
function refreshScopeAfterDelete(cwd, id) {
db.run("DELETE FROM core_decisions WHERE scope = ? AND session_id = ?", [cwd, id]);
const latest = all("SELECT * FROM sessions WHERE cwd = ? ORDER BY ts DESC LIMIT 1", [cwd])[0];
if (!latest) {
db.run("DELETE FROM core WHERE scope = ?", [cwd]);
return 0;
}
db.run(
`INSERT INTO core (scope,state,next_steps,files,updated_at) VALUES (?,?,?,?,?)
ON CONFLICT(scope) DO UPDATE SET state=excluded.state,next_steps=excluded.next_steps,files=excluded.files,updated_at=excluded.updated_at`,
[cwd, clip(latest.summary, 300), JSON.stringify(parseArr(latest.next_steps).slice(0, 6)), JSON.stringify(parseArr(latest.files).slice(0, 8)), new Date().toISOString()]
);
return 1;
}
function removeGeneratedHandoff(cwd) {
const file = handoffPath(cwd);
let text = "";
try { text = fs.readFileSync(file, "utf-8"); } catch { return; }
if (text.startsWith("<!-- 本文件由「Codex 插件市场 · 会话记忆」自动生成与覆盖,请勿手动编辑 -->")) {
fs.rmSync(file, { force: true });
}
}
function canWriteGeneratedHandoff(cwd) {
let text = "";
try { text = fs.readFileSync(handoffPath(cwd), "utf-8"); } catch { return true; }
return text.startsWith("<!-- 本文件由「Codex 插件市场 · 会话记忆」自动生成与覆盖,请勿手动编辑 -->");
}
// 新版首启:先把已有 sessions 迁移进结构化 core,再把所有已知项目 AGENTS.md 升级为核心块格式
function refreshAllCores() {
try {
@@ -738,16 +827,19 @@ function start(opts = {}) {
if (opts.log) log = opts.log;
if (opts.notify) notify = opts.notify;
loadCfg();
// 先把已存在的会话标记为「已见」,这样只有启动后【新开】的会话才触发「即刻结算上一个」
try {
for (const it of sessions.listConversations({ includeSubagent: true })) seenRollouts.add(it.path);
} catch {}
const markSeen = () => {
// 标记已存在会话需要递归扫描 ~/.codex/sessions,启动时可延后,避免阻塞主进程响应点击。
try {
for (const it of sessions.listConversations({ includeSubagent: true })) seenRollouts.add(it.path);
} catch {}
};
// 监视器常开(总开关只控制是否处理),这样开关切换无需重启
try {
fs.mkdirSync(sessions.SESSIONS_DIR, { recursive: true });
watcher = fs.watch(sessions.SESSIONS_DIR, { recursive: true }, (_ev, name) => {
const sessionsDir = sessions.getSessionsDir();
fs.mkdirSync(sessionsDir, { recursive: true });
watcher = fs.watch(sessionsDir, { recursive: true }, (_ev, name) => {
if (!name || !/rollout-.*\.jsonl$/.test(String(name))) return;
const full = path.join(sessions.SESSIONS_DIR, String(name));
const full = path.join(sessionsDir, String(name));
// 「开新会话即刻结算上一个」:首次见到某 rollout 文件 = 新会话开始 → 立刻把其它待处理的(多半是刚关掉的那个)先存好
if (!seenRollouts.has(full)) {
seenRollouts.add(full);
@@ -759,7 +851,15 @@ function start(opts = {}) {
log("会话记忆:无法监视 sessions 目录 " + (e.message || e));
}
sweepTimer = setInterval(sweep, 5000);
backfill().catch((e) => log("会话记忆:启动扫描失败 " + (e.message || e)));
if (opts.startupScan === false) return;
const startupScan = () => {
if (!loadCfg().enabled) return;
markSeen();
backfill().catch((e) => log("会话记忆:启动扫描失败 " + (e.message || e)));
};
const delay = Number(opts.startupDelayMs) || 0;
if (delay > 0) setTimeout(startupScan, delay);
else startupScan();
}
function stop() {
@@ -771,6 +871,14 @@ function stop() {
flushDb(); // 退出前把内存库落盘
}
function restartWatching() {
stop();
dirty.clear();
seenRollouts.clear();
queue.length = 0;
start();
}
// ---------------------------------------------------------------------------
// 给 UI 的接口
// ---------------------------------------------------------------------------
@@ -856,15 +964,61 @@ async function deleteRow(id) {
notify();
}
async function deleteSession(id, cwd) {
deletedSessionIds.add(String(id || ""));
await ensureDb();
const rows = all("SELECT cwd, rollout_path FROM sessions WHERE id = ?", [id]);
for (const row of rows) {
if (row.rollout_path) {
dirty.delete(row.rollout_path);
seenRollouts.delete(row.rollout_path);
for (let i = queue.length - 1; i >= 0; i--) if (queue[i] === row.rollout_path) queue.splice(i, 1);
}
}
const storedScopes = Array.from(new Set(rows.map((row) => row.cwd).filter(Boolean)));
const scopes = storedScopes.length ? storedScopes : (cwd ? [cwd] : []);
const snapshot = Buffer.from(db.export());
try {
db.run("BEGIN");
db.run("DELETE FROM sessions WHERE id = ?", [id]);
for (const scope of scopes) refreshScopeAfterDelete(scope, id);
db.run("COMMIT");
flushDbStrict();
} catch (e) {
try { db.run("ROLLBACK"); } catch {}
try { db.close(); } catch {}
db = new SQLmod.Database(snapshot);
throw e;
}
for (const scope of scopes) removeArchiveSession(scope, id);
for (const scope of scopes) {
if (!scope) continue;
try { if (!fs.statSync(scope).isDirectory()) continue; } catch { continue; }
try {
if (all("SELECT 1 FROM sessions WHERE cwd = ? LIMIT 1", [scope]).length) {
writeProjectCore(scope);
if (canWriteGeneratedHandoff(scope)) writeHandoff(scope);
} else {
removeProjectCore(scope);
removeGeneratedHandoff(scope);
}
} catch (e) { log("会话记忆派生文件清理失败:" + (e.message || e)); }
}
notify();
return { id, scopes };
}
module.exports = {
start,
stop,
restartWatching,
statusInfo,
list,
setEnabled,
toggleProject,
resummarize,
deleteRow,
deleteSession,
handoffPath,
renderProjectCore,
writeProjectCore,