fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)
This commit is contained in:
+162
-8
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user