921 lines
29 KiB
JavaScript
921 lines
29 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
let runtimeRoot = "";
|
|
let discoverCodexProjectCandidates;
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-discovery-"));
|
|
({ discoverCodexProjectCandidates } = await import("../local-agent/codex-session-discovery.mjs"));
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates prefers Codex sqlite indexes and session names over raw rollout fallback", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex");
|
|
const now = new Date("2026-03-30T12:45:00+08:00");
|
|
await mkdir(codexRoot, { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const sessionIndexPath = path.join(codexRoot, "session_index.jsonl");
|
|
const globalStatePath = path.join(codexRoot, ".codex-global-state.json");
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
const insertThread = stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, agent_nickname, agent_role, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, '0.118.0', '', ?, ?, 'enabled', 'gpt-5.4', 'medium')
|
|
`);
|
|
insertThread.run(
|
|
"019d3bossmain",
|
|
path.join(codexRoot, "sessions/2026/03/30/rollout-boss-main.jsonl"),
|
|
1774845600,
|
|
1774845618,
|
|
"desktop",
|
|
"openai",
|
|
"/Users/kris/code/boss",
|
|
"Boss 主线程",
|
|
"workspace-write",
|
|
"never",
|
|
0,
|
|
null,
|
|
null,
|
|
);
|
|
insertThread.run(
|
|
"019d3bossworker",
|
|
path.join(codexRoot, "sessions/2026/03/30/rollout-boss-worker.jsonl"),
|
|
1774845620,
|
|
1774845630,
|
|
"desktop",
|
|
"openai",
|
|
"/Users/kris/code/boss",
|
|
"你是只读分析子线程",
|
|
"workspace-write",
|
|
"never",
|
|
0,
|
|
"Sagan",
|
|
"explorer",
|
|
);
|
|
insertThread.run(
|
|
"019d3yuandiexplorer",
|
|
path.join(codexRoot, "sessions/2026/03/30/rollout-yuandi-explorer.jsonl"),
|
|
1774845760,
|
|
1774845776,
|
|
"desktop",
|
|
"openai",
|
|
"/Users/kris/.codex/worktrees/tmp123/yuandi",
|
|
"Yuandi 子线程",
|
|
"workspace-write",
|
|
"never",
|
|
0,
|
|
"Epicurus",
|
|
"explorer",
|
|
);
|
|
insertThread.run(
|
|
"019d3oldsession",
|
|
path.join(codexRoot, "sessions/2026/03/27/rollout-too-old.jsonl"),
|
|
1774584000,
|
|
1774584000,
|
|
"desktop",
|
|
"openai",
|
|
"/Users/kris/code/old-project",
|
|
"Old Session",
|
|
"workspace-write",
|
|
"never",
|
|
0,
|
|
null,
|
|
null,
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
const insertLog = logsDb.prepare(`
|
|
INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes)
|
|
VALUES (?, 0, 'info', 'codex', ?, 0)
|
|
`);
|
|
insertLog.run(1774845618, "019d3bossmain");
|
|
insertLog.run(1774845630, "019d3bossworker");
|
|
insertLog.run(1774845776, "019d3yuandiexplorer");
|
|
logsDb.close();
|
|
|
|
await writeFile(
|
|
sessionIndexPath,
|
|
[
|
|
JSON.stringify({
|
|
id: "019d3bossmain",
|
|
thread_name: "Boss 主线程",
|
|
updated_at: "2026-03-30T04:40:18.000000Z",
|
|
}),
|
|
JSON.stringify({
|
|
id: "019d3yuandiexplorer",
|
|
thread_name: "Epicurus",
|
|
updated_at: "2026-03-30T04:42:56.000000Z",
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
globalStatePath,
|
|
JSON.stringify(
|
|
{
|
|
"thread-workspace-root-hints": {
|
|
"019d3yuandiexplorer": "/Users/kris/code/yuandi",
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
sessionIndexPath,
|
|
globalStatePath,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
assert.deepEqual(discovered.projects, ["boss", "yuandi"]);
|
|
assert.equal(discovered.guiConnected, true);
|
|
assert.equal(discovered.projectCandidates.length, 2);
|
|
|
|
const bossSession = discovered.projectCandidates.find((item) => item.threadId === "019d3bossmain");
|
|
const bossWorker = discovered.projectCandidates.find((item) => item.threadId === "019d3bossworker");
|
|
const yuandiSession = discovered.projectCandidates.find((item) => item.threadId === "019d3yuandiexplorer");
|
|
assert.ok(bossSession);
|
|
assert.equal(bossWorker, undefined, "subagent/explorer threads should be filtered when a primary thread exists for the same folder");
|
|
assert.ok(yuandiSession);
|
|
assert.equal(bossSession?.folderName, "boss");
|
|
assert.equal(bossSession?.codexFolderRef, "/Users/kris/code/boss");
|
|
assert.equal(bossSession?.codexThreadRef, "019d3bossmain");
|
|
assert.equal(bossSession?.threadDisplayName, "Boss 主线程");
|
|
assert.equal(yuandiSession?.folderName, "yuandi");
|
|
assert.equal(yuandiSession?.threadDisplayName, "Epicurus");
|
|
assert.equal(yuandiSession?.codexFolderRef, "/Users/kris/code/yuandi");
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates merges session-only Codex threads when state db is partially stale", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-session-merge");
|
|
const now = new Date("2026-05-02T10:00:00+08:00");
|
|
await mkdir(codexRoot, { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const sessionIndexPath = path.join(codexRoot, "session_index.jsonl");
|
|
const globalStatePath = path.join(codexRoot, ".codex-global-state.json");
|
|
const sessionsDir = path.join(codexRoot, "sessions");
|
|
const sessionOnlyDir = path.join(sessionsDir, "2026", "05", "02");
|
|
await mkdir(sessionOnlyDir, { recursive: true });
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, agent_nickname, agent_role, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, 1777686800, 1777686800, 'vscode', 'openai', ?, 'Boss 主线程', 'workspace-write', 'never', 0, 1, 0, '0.118.0', '', '', '', 'enabled', 'gpt-5.4', 'medium')
|
|
`).run(
|
|
"019d-state-thread",
|
|
path.join(sessionsDir, "2026/05/02/rollout-state-thread.jsonl"),
|
|
"/Users/kris/code/boss",
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
logsDb.prepare(`
|
|
INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes)
|
|
VALUES (1777686800, 0, 'info', 'codex', '019d-state-thread', 0)
|
|
`).run();
|
|
logsDb.close();
|
|
|
|
await writeFile(sessionIndexPath, "", "utf8");
|
|
await writeFile(
|
|
globalStatePath,
|
|
JSON.stringify({ "thread-workspace-root-hints": {} }, null, 2),
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
path.join(sessionOnlyDir, "rollout-2026-05-02T09-53-45-019d-session-only.jsonl"),
|
|
`${JSON.stringify({
|
|
timestamp: "2026-05-02T01:53:45.000Z",
|
|
type: "session_meta",
|
|
payload: {
|
|
id: "019d-session-only",
|
|
cwd: "/Users/kris/code/boss-regression-smoke",
|
|
timestamp: "2026-05-02T01:53:45.000Z",
|
|
},
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
sessionIndexPath,
|
|
globalStatePath,
|
|
sessionsDir,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
assert.deepEqual(discovered.projects, ["boss", "boss-regression-smoke"]);
|
|
assert.equal(discovered.guiConnected, true);
|
|
assert.ok(
|
|
discovered.projectCandidates.some((item) => item.threadId === "019d-session-only"),
|
|
"expected session-only Codex threads to be imported even when state db has other rows",
|
|
);
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates collapses duplicate final assistant records from the same rollout turn", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-rollout-dedupe");
|
|
const sessionsDir = path.join(codexRoot, "sessions");
|
|
const rolloutDir = path.join(sessionsDir, "2026", "05", "02");
|
|
const rolloutPath = path.join(
|
|
rolloutDir,
|
|
"rollout-2026-05-02T10-10-00-019drolloutdedupe.jsonl",
|
|
);
|
|
await mkdir(rolloutDir, { recursive: true });
|
|
|
|
const replyBody = "BOSS回归APP消息已收到。";
|
|
await writeFile(
|
|
rolloutPath,
|
|
[
|
|
JSON.stringify({
|
|
timestamp: "2026-05-02T02:10:00.000Z",
|
|
type: "session_meta",
|
|
payload: {
|
|
id: "019drolloutdedupe",
|
|
cwd: "/Users/kris/code/boss-regression-smoke",
|
|
timestamp: "2026-05-02T02:10:00.000Z",
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: "2026-05-02T02:10:36.311Z",
|
|
type: "event_msg",
|
|
payload: {
|
|
type: "agent_message",
|
|
message: replyBody,
|
|
phase: "final_answer",
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: "2026-05-02T02:10:36.312Z",
|
|
type: "response_item",
|
|
payload: {
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: replyBody }],
|
|
},
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
sessionsDir,
|
|
stateDbPath: path.join(codexRoot, "missing-state.sqlite"),
|
|
logsDbPath: path.join(codexRoot, "missing-logs.sqlite"),
|
|
sessionIndexPath: path.join(codexRoot, "missing-session-index.jsonl"),
|
|
globalStatePath: path.join(codexRoot, "missing-global-state.json"),
|
|
lookbackHours: 24,
|
|
now: new Date("2026-05-02T10:30:00+08:00"),
|
|
});
|
|
|
|
const smokeThread = discovered.projectCandidates.find(
|
|
(candidate) => candidate.threadId === "019drolloutdedupe",
|
|
);
|
|
assert.ok(smokeThread, "expected rollout-only smoke thread to be discovered");
|
|
assert.equal(smokeThread?.recentAssistantMessages?.length, 1);
|
|
assert.equal(smokeThread?.recentAssistantMessages?.[0]?.body, replyBody);
|
|
assert.equal(smokeThread?.recentAssistantMessages?.[0]?.phase, "final_answer");
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates excludes read-only threads even when they are the newest primary thread", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-readonly");
|
|
const now = new Date("2026-04-05T12:00:00+08:00");
|
|
await mkdir(codexRoot, { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const sessionIndexPath = path.join(codexRoot, "session_index.jsonl");
|
|
const globalStatePath = path.join(codexRoot, ".codex-global-state.json");
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
const insertThread = stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, agent_nickname, agent_role, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, ?, ?, 'desktop', 'openai', ?, ?, ?, 'never', 0, 1, 0, '0.118.0', '', '', '', 'enabled', 'gpt-5.4', 'medium')
|
|
`);
|
|
insertThread.run(
|
|
"019d-boss-writable",
|
|
path.join(codexRoot, "sessions/2026/04/05/rollout-boss-writable.jsonl"),
|
|
1775322000,
|
|
1775322060,
|
|
"/Users/kris/code/boss",
|
|
"Boss 可写线程",
|
|
'{"type":"workspace-write"}',
|
|
);
|
|
insertThread.run(
|
|
"019d-boss-readonly",
|
|
path.join(codexRoot, "sessions/2026/04/05/rollout-boss-readonly.jsonl"),
|
|
1775322120,
|
|
1775322180,
|
|
"/Users/kris/code/boss",
|
|
"Boss 只读线程",
|
|
'{"type":"read-only"}',
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
const insertLog = logsDb.prepare(`
|
|
INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes)
|
|
VALUES (?, 0, 'info', 'codex', ?, 0)
|
|
`);
|
|
insertLog.run(1775322060, "019d-boss-writable");
|
|
insertLog.run(1775322180, "019d-boss-readonly");
|
|
logsDb.close();
|
|
|
|
await writeFile(
|
|
sessionIndexPath,
|
|
[
|
|
JSON.stringify({
|
|
id: "019d-boss-writable",
|
|
thread_name: "Boss 可写线程",
|
|
updated_at: "2026-04-05T04:21:00.000000Z",
|
|
}),
|
|
JSON.stringify({
|
|
id: "019d-boss-readonly",
|
|
thread_name: "Boss 只读线程",
|
|
updated_at: "2026-04-05T04:23:00.000000Z",
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
globalStatePath,
|
|
JSON.stringify({ "thread-workspace-root-hints": {} }, null, 2),
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
sessionIndexPath,
|
|
globalStatePath,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
assert.deepEqual(discovered.projects, ["boss"]);
|
|
assert.equal(discovered.projectCandidates.length, 1);
|
|
assert.equal(discovered.projectCandidates[0]?.threadId, "019d-boss-writable");
|
|
assert.equal(discovered.projectCandidates[0]?.threadDisplayName, "Boss 可写线程");
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates falls back to workspace folder when thread title leaks internal prompt text", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-prompt-title");
|
|
const now = new Date("2026-04-24T11:00:00+08:00");
|
|
await mkdir(codexRoot, { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const sessionIndexPath = path.join(codexRoot, "session_index.jsonl");
|
|
const globalStatePath = path.join(codexRoot, ".codex-global-state.json");
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, ?, ?, 'desktop', 'openai', ?, ?, 'workspace-write', 'never', 0, 1, 0, '0.118.0', '', 'enabled', 'gpt-5.4', 'medium')
|
|
`).run(
|
|
"019d-prompt-main",
|
|
path.join(codexRoot, "sessions/2026/04/24/rollout-prompt-main.jsonl"),
|
|
1776998400,
|
|
1776998460,
|
|
"/Users/kris/code/boss",
|
|
"你当前接手的项目根目录是:",
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
logsDb.prepare(`
|
|
INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes)
|
|
VALUES (?, 0, 'info', 'codex', ?, 0)
|
|
`).run(1776998460, "019d-prompt-main");
|
|
logsDb.close();
|
|
|
|
await writeFile(
|
|
sessionIndexPath,
|
|
JSON.stringify({
|
|
id: "019d-prompt-main",
|
|
thread_name: "你现在接手的项目根目录是 /Users/kris/code/boss。",
|
|
updated_at: "2026-04-24T03:21:00.000000Z",
|
|
}) + "\n",
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
globalStatePath,
|
|
JSON.stringify({ "thread-workspace-root-hints": {} }, null, 2),
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
sessionIndexPath,
|
|
globalStatePath,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
assert.deepEqual(discovered.projects, ["boss"]);
|
|
assert.equal(discovered.projectCandidates.length, 1);
|
|
assert.equal(discovered.projectCandidates[0]?.threadDisplayName, "boss");
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates mirrors recent desktop assistant replies from rollout files", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-message-sync");
|
|
const now = new Date("2026-04-20T18:00:00+08:00");
|
|
await mkdir(path.join(codexRoot, "sessions/2026/04/20"), { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const rolloutPath = path.join(codexRoot, "sessions/2026/04/20/rollout-boss-main.jsonl");
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, ?, ?, 'desktop', 'openai', ?, ?, 'workspace-write', 'never', 0, 1, 0, '0.118.0', '', 'enabled', 'gpt-5.4', 'medium')
|
|
`).run(
|
|
"019d-message-main",
|
|
rolloutPath,
|
|
1776680000,
|
|
1776680100,
|
|
"/Users/kris/code/boss",
|
|
"Boss 主线程",
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
logsDb
|
|
.prepare("INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes) VALUES (?, 0, 'info', 'codex', ?, 0)")
|
|
.run(1776680100, "019d-message-main");
|
|
logsDb.close();
|
|
|
|
const assistantText = "桌面线程已经完成实时同步修复。";
|
|
const assistantSentAt = "2026-04-20T09:34:56.000Z";
|
|
await writeFile(
|
|
rolloutPath,
|
|
[
|
|
JSON.stringify({
|
|
type: "session_meta",
|
|
payload: {
|
|
id: "019d-message-main",
|
|
cwd: "/Users/kris/code/boss",
|
|
timestamp: "2026-04-20T09:30:00.000Z",
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: assistantSentAt,
|
|
type: "event_msg",
|
|
payload: {
|
|
type: "agent_message",
|
|
message: assistantText,
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: assistantSentAt,
|
|
type: "response_item",
|
|
payload: {
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: assistantText }],
|
|
},
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
assert.equal(discovered.projectCandidates.length, 1);
|
|
const candidate = discovered.projectCandidates[0];
|
|
assert.ok(candidate);
|
|
assert.deepEqual(candidate?.recentAssistantMessages, [
|
|
{
|
|
messageId: `codex-thread:019d-message-main:${assistantSentAt}:${crypto.createHash("sha1").update(assistantText).digest("hex").slice(0, 12)}`,
|
|
body: assistantText,
|
|
sentAt: assistantSentAt,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("discoverCodexProjectCandidates preserves assistant reply phase for process folding", async () => {
|
|
await setup();
|
|
|
|
const codexRoot = path.join(runtimeRoot, ".codex-message-phase");
|
|
const now = new Date("2026-04-20T18:30:00+08:00");
|
|
await mkdir(path.join(codexRoot, "sessions/2026/04/20"), { recursive: true });
|
|
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
|
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
|
const rolloutPath = path.join(codexRoot, "sessions/2026/04/20/rollout-boss-main.jsonl");
|
|
|
|
const stateDb = new DatabaseSync(stateDbPath);
|
|
stateDb.exec(`
|
|
CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT
|
|
);
|
|
`);
|
|
stateDb.prepare(`
|
|
INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
cli_version, first_user_message, memory_mode, model, reasoning_effort
|
|
) VALUES (?, ?, ?, ?, 'desktop', 'openai', ?, ?, 'workspace-write', 'never', 0, 1, 0, '0.118.0', '', 'enabled', 'gpt-5.4', 'medium')
|
|
`).run(
|
|
"019d-message-phase",
|
|
rolloutPath,
|
|
1776680000,
|
|
1776681000,
|
|
"/Users/kris/code/boss",
|
|
"Boss 主线程",
|
|
);
|
|
stateDb.close();
|
|
|
|
const logsDb = new DatabaseSync(logsDbPath);
|
|
logsDb.exec(`
|
|
CREATE TABLE logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts INTEGER NOT NULL,
|
|
ts_nanos INTEGER NOT NULL,
|
|
level TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
feedback_log_body TEXT,
|
|
module_path TEXT,
|
|
file TEXT,
|
|
line INTEGER,
|
|
thread_id TEXT,
|
|
process_uuid TEXT,
|
|
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
logsDb
|
|
.prepare("INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes) VALUES (?, 0, 'info', 'codex', ?, 0)")
|
|
.run(1776681000, "019d-message-phase");
|
|
logsDb.close();
|
|
|
|
const processText = "我先检查聊天折叠链路,确认过程消息不会直接展开。";
|
|
const finalText = "已完成折叠修复,过程消息会收进按钮里,未读只增加一次。";
|
|
const processSentAt = "2026-04-20T10:28:10.000Z";
|
|
const finalSentAt = "2026-04-20T10:29:30.000Z";
|
|
await writeFile(
|
|
rolloutPath,
|
|
[
|
|
JSON.stringify({
|
|
type: "session_meta",
|
|
payload: {
|
|
id: "019d-message-phase",
|
|
cwd: "/Users/kris/code/boss",
|
|
timestamp: "2026-04-20T10:20:00.000Z",
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: processSentAt,
|
|
type: "event_msg",
|
|
payload: {
|
|
type: "agent_message",
|
|
message: processText,
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: processSentAt,
|
|
type: "response_item",
|
|
payload: {
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: processText }],
|
|
phase: "commentary",
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
timestamp: finalSentAt,
|
|
type: "response_item",
|
|
payload: {
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: finalText }],
|
|
phase: "final_answer",
|
|
},
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
|
|
const discovered = await discoverCodexProjectCandidates({
|
|
stateDbPath,
|
|
logsDbPath,
|
|
lookbackHours: 24,
|
|
now,
|
|
});
|
|
|
|
const candidate = discovered.projectCandidates[0];
|
|
assert.ok(candidate);
|
|
assert.deepEqual(candidate.recentAssistantMessages, [
|
|
{
|
|
messageId: `codex-thread:019d-message-phase:${processSentAt}:${crypto.createHash("sha1").update(processText).digest("hex").slice(0, 12)}`,
|
|
body: processText,
|
|
sentAt: processSentAt,
|
|
phase: "commentary",
|
|
},
|
|
{
|
|
messageId: `codex-thread:019d-message-phase:${finalSentAt}:${crypto.createHash("sha1").update(finalText).digest("hex").slice(0, 12)}`,
|
|
body: finalText,
|
|
sentAt: finalSentAt,
|
|
phase: "final_answer",
|
|
},
|
|
]);
|
|
});
|