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
+166
View File
@@ -0,0 +1,166 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const Module = require("node:module");
const childProcess = require("node:child_process");
const initSqlJs = require("sql.js");
const PROVIDER_MODULE = path.resolve(__dirname, "../electron/lib/provider_sync.js");
const DESKTOP_MODULE = path.resolve(__dirname, "../electron/lib/codex_desktop.js");
function writeRollout(root, id) {
const dir = path.join(root, "sessions", "2026", "08", "09");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, `rollout-${id}.jsonl`), [
JSON.stringify({ type: "session_meta", payload: { session_id: id, cwd: "C:/work/demo", timestamp: "2026-08-09T10:00:00.000Z", model_provider: "old" } }),
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: "Windows restore test" } }),
].join("\n") + "\n");
}
// subagent 会话的 session_meta.payload.source 是对象({subagent:{thread_spawn:{...}}}),
// 不是字符串。曾导致 Windows(无 sqlite3 CLI,走 sql.js 绑定)整批 upsert 抛错回滚,
// state 库 0 线程而侧栏 catalog 正常,Codex 启动 full reconcile 后会话"闪现后全部消失"。
function writeSubagentRollout(root, id, parentId) {
const dir = path.join(root, "sessions", "2026", "08", "09");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, `rollout-${id}.jsonl`), [
JSON.stringify({
type: "session_meta",
payload: {
session_id: id,
cwd: "C:/work/demo",
timestamp: "2026-08-09T11:00:00.000Z",
model_provider: "old",
source: { subagent: { thread_spawn: { parent_thread_id: parentId, depth: 1, agent_path: null, agent_nickname: "Dirac", agent_role: null } } },
thread_source: "subagent",
},
}),
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: "subagent task" } }),
].join("\n") + "\n");
}
test("会话恢复在没有 sqlite3 时插入线程并创建侧栏目录", { concurrency: false }, async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-session-restore-"));
const dataDir = path.join(root, "codex");
const userData = path.join(root, "app-data");
const id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const originalLoad = Module._load;
const originalSpawnSync = childProcess.spawnSync;
const originalBundleRoot = global.__BUNDLE_ROOT__;
try {
fs.mkdirSync(userData, { recursive: true });
fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
fs.writeFileSync(path.join(dataDir, "config.toml"), 'model_provider = "openai"\n');
writeRollout(dataDir, id);
Module._load = function (request, parent, isMain) {
if (request === "electron") return { app: { getPath: () => userData } };
return originalLoad.call(this, request, parent, isMain);
};
childProcess.spawnSync = function (command, args, options) {
if (command === "sqlite3") return { error: new Error("sqlite3 not installed"), status: null, stdout: "", stderr: "" };
return originalSpawnSync.call(this, command, args, options);
};
delete require.cache[PROVIDER_MODULE];
delete require.cache[DESKTOP_MODULE];
global.__BUNDLE_ROOT__ = path.resolve(__dirname, "..");
const provider = require(PROVIDER_MODULE);
const desktop = require(DESKTOP_MODULE);
desktop.codexRunning = () => false;
provider.setCodexDataDir(dataDir);
const result = await provider.syncProviderToCurrent(() => {});
assert.equal(result.insertedRows, 1);
assert.equal(result.catalogRebuilt, 1);
assert.equal(result.stateDbCreated, 1);
assert.equal(result.stateDbCreateError, "");
assert.equal(fs.existsSync(path.join(dataDir, "sqlite", "codex-dev.db")), true);
assert.equal(fs.existsSync(path.join(dataDir, "session_index.jsonl")), true);
assert.equal(fs.existsSync(path.join(dataDir, "state_5.sqlite")), true);
const SQL = await initSqlJs({ locateFile: (file) => path.join(__dirname, "../node_modules/sql.js/dist", file) });
const restoredState = new SQL.Database(fs.readFileSync(path.join(dataDir, "state_5.sqlite")));
assert.equal(restoredState.exec("SELECT COUNT(*) FROM threads")[0].values[0][0], 1);
assert.equal(restoredState.exec("SELECT has_user_event FROM threads WHERE id='" + id + "'")[0].values[0][0], 1);
assert.equal(restoredState.exec("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('thread_sections','thread_dynamic_tools','thread_spawn_edges','backfill_state','external_agent_config_imports','remote_control_enrollments')")[0].values[0][0], 6);
assert.equal(restoredState.exec("SELECT COUNT(*) FROM _sqlx_migrations")[0].values[0][0], 45);
restoredState.close();
const catalog = new SQL.Database(fs.readFileSync(path.join(dataDir, "sqlite", "codex-dev.db")));
assert.equal(catalog.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 1);
catalog.close();
} finally {
Module._load = originalLoad;
childProcess.spawnSync = originalSpawnSync;
global.__BUNDLE_ROOT__ = originalBundleRoot;
delete require.cache[PROVIDER_MODULE];
delete require.cache[DESKTOP_MODULE];
fs.rmSync(root, { recursive: true, force: true });
}
});
test("subagent source 对象不再让 Windows(sql.js) 路径整批失败,且自建 catalog 库带 user_version", { concurrency: false }, async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-session-restore-"));
const dataDir = path.join(root, "codex");
const userData = path.join(root, "app-data");
const id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
const subId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
const originalLoad = Module._load;
const originalSpawnSync = childProcess.spawnSync;
const originalBundleRoot = global.__BUNDLE_ROOT__;
try {
fs.mkdirSync(userData, { recursive: true });
fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
fs.writeFileSync(path.join(dataDir, "config.toml"), 'model_provider = "openai"\n');
writeRollout(dataDir, id);
writeSubagentRollout(dataDir, subId, id);
Module._load = function (request, parent, isMain) {
if (request === "electron") return { app: { getPath: () => userData } };
return originalLoad.call(this, request, parent, isMain);
};
childProcess.spawnSync = function (command, args, options) {
if (command === "sqlite3") return { error: new Error("sqlite3 not installed"), status: null, stdout: "", stderr: "" };
return originalSpawnSync.call(this, command, args, options);
};
delete require.cache[PROVIDER_MODULE];
delete require.cache[DESKTOP_MODULE];
global.__BUNDLE_ROOT__ = path.resolve(__dirname, "..");
const provider = require(PROVIDER_MODULE);
const desktop = require(DESKTOP_MODULE);
desktop.codexRunning = () => false;
provider.setCodexDataDir(dataDir);
const result = await provider.syncProviderToCurrent(() => {}, () => {});
// 两条线程都必须进 state 库(修复前 subagent 的 source 对象会让整批回滚,insertedRows=0)
assert.equal(result.insertedRows, 2);
const SQL = await initSqlJs({ locateFile: (file) => path.join(__dirname, "../node_modules/sql.js/dist", file) });
const state = new SQL.Database(fs.readFileSync(path.join(dataDir, "state_5.sqlite")));
assert.equal(state.exec("SELECT COUNT(*) FROM threads")[0].values[0][0], 2);
// subagent 的 source 以 JSON 字符串入库(与 Codex 自身存储一致),而不是 '[object Object]'
const subSource = state.exec(`SELECT source FROM threads WHERE id='${subId}'`)[0].values[0][0];
assert.ok(subSource.startsWith('{"subagent"'), `source 应为 JSON 字符串,实际: ${subSource}`);
assert.ok(subSource.includes("Dirac"));
state.close();
// 自建 codex-dev.db 必须带 user_version(对齐 Codex 迁移链 v30),
// 否则 Codex 重跑迁移 v27/28/29 会置空 last_full_reconciled_at 触发 full reconcile
const catalog = new SQL.Database(fs.readFileSync(path.join(dataDir, "sqlite", "codex-dev.db")));
const uv = catalog.exec("PRAGMA user_version")[0].values[0][0];
assert.equal(uv, 30);
assert.equal(catalog.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 2);
catalog.close();
} finally {
Module._load = originalLoad;
childProcess.spawnSync = originalSpawnSync;
global.__BUNDLE_ROOT__ = originalBundleRoot;
delete require.cache[PROVIDER_MODULE];
delete require.cache[DESKTOP_MODULE];
fs.rmSync(root, { recursive: true, force: true });
}
});