diff --git a/local-agent/codex-task-runner.mjs b/local-agent/codex-task-runner.mjs index 5406eb7..4fbc19c 100644 --- a/local-agent/codex-task-runner.mjs +++ b/local-agent/codex-task-runner.mjs @@ -1,3 +1,5 @@ +import os from "node:os"; +import { readFileSync } from "node:fs"; import { constants } from "node:fs"; import { access, stat } from "node:fs/promises"; import { DatabaseSync } from "node:sqlite"; @@ -21,6 +23,20 @@ function resolveResumeTarget(config, task) { }; } +function defaultCodexPath(relativePath) { + return resolve(os.homedir(), ".codex", relativePath); +} + +function loadThreadWorkspaceHints(globalStatePath) { + try { + const raw = readFileSync(resolve(globalStatePath), "utf8"); + const parsed = JSON.parse(raw); + return new Map(Object.entries(parsed["thread-workspace-root-hints"] ?? {})); + } catch { + return new Map(); + } +} + function shouldPreflightResumeTask(task) { const taskType = String(task?.taskType || "").trim(); if (taskType === "dispatch_execution") { @@ -47,7 +63,7 @@ function buildStructuredTaskBindingError(code, message, details) { } function inspectCodexThreadBinding(config, targetThreadRef, targetFolderRef) { - const stateDbPath = trimToDefined(config?.codexStateDbPath); + const stateDbPath = trimToDefined(config?.codexStateDbPath || defaultCodexPath("state_5.sqlite")); if (!stateDbPath) { return { status: "skipped", @@ -66,7 +82,10 @@ function inspectCodexThreadBinding(config, targetThreadRef, targetFolderRef) { }; } - const threadCwd = trimToDefined(row.cwd) || ""; + const workspaceHints = loadThreadWorkspaceHints( + trimToDefined(config?.codexGlobalStatePath || defaultCodexPath(".codex-global-state.json")), + ); + const threadCwd = trimToDefined(workspaceHints.get(targetThreadRef)) || trimToDefined(row.cwd) || ""; if (threadCwd && resolve(threadCwd) !== resolve(targetFolderRef)) { return { status: "mismatch", @@ -129,22 +148,6 @@ export async function prepareCodexTaskExecution(config, task, outputFile) { }; } - if (bindingInspection.status === "mismatch") { - return { - ok: false, - error: buildStructuredTaskBindingError( - "LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH", - `LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH: 目标线程绑定的 cwd 与当前目录不一致,已拒绝 codex exec resume。cwd=${resumeTarget.cwd} liveCwd=${bindingInspection.threadCwd}`, - { - cwd: resumeTarget.cwd, - liveCwd: bindingInspection.threadCwd, - targetThreadRef, - targetCodexFolderRef: resumeTarget.targetFolderRef, - }, - ), - }; - } - try { const folderStat = await stat(resumeTarget.cwd); if (!folderStat.isDirectory()) { @@ -166,6 +169,22 @@ export async function prepareCodexTaskExecution(config, task, outputFile) { }; } + if (bindingInspection.status === "mismatch") { + return { + ok: false, + error: buildStructuredTaskBindingError( + "LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH", + `LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH: 目标线程绑定的 cwd 与当前目录不一致,已拒绝 codex exec resume。cwd=${resumeTarget.cwd} liveCwd=${bindingInspection.threadCwd}`, + { + cwd: resumeTarget.cwd, + liveCwd: bindingInspection.threadCwd, + targetThreadRef, + targetCodexFolderRef: resumeTarget.targetFolderRef, + }, + ), + }; + } + return { ok: true, execution: buildCodexTaskExecution(config, task, outputFile), diff --git a/tests/local-agent-codex-task-runner.test.mjs b/tests/local-agent-codex-task-runner.test.mjs index 8e21cb2..4c54650 100644 --- a/tests/local-agent-codex-task-runner.test.mjs +++ b/tests/local-agent-codex-task-runner.test.mjs @@ -170,10 +170,18 @@ test("master agent reply without target thread stays on ephemeral exec", () => { test("conversation reply preflight fails closed when target cwd is missing", async () => { const missingFolder = "/tmp/boss-local-agent-missing-workdir"; + const stateDbPath = await createCodexStateDb([ + { + id: "019d-thread-real", + cwd: "/Users/kris/code/boss", + title: "Real thread", + }, + ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", + codexStateDbPath: stateDbPath, }, { taskType: "conversation_reply",