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", }, ]); });