diff --git a/plugins/pi-mail-bridge/src/worker.mjs b/plugins/pi-mail-bridge/src/worker.mjs index 1bc4dca..93ab272 100644 --- a/plugins/pi-mail-bridge/src/worker.mjs +++ b/plugins/pi-mail-bridge/src/worker.mjs @@ -51,6 +51,7 @@ import { buildMailPrompt, lastAssistantText, replySubject, relayKeyFor, describe import { planNamingSync, planWriteBack } from './naming.mjs'; import { resolveWorkspaceCwd, ensureCwd } from '../lib/workspace.js'; import { piMailFallback } from './paths.mjs'; +import { resolveSessionReuse } from './turn-cwd.mjs'; import { modelAttemptOrder, renderFailureReport } from '../lib/model-scope.js'; import { explicitSends, shouldSkipAutoRelay } from '../lib/relay-dedup.js'; import { autoRelayDecision } from '../lib/relay-policy.js'; @@ -341,9 +342,20 @@ async function loadSession(mailTools) { // 不消费它的分支 —— 那正是"回报了没用的 cwd"那个缺口的根子。 // 判定唯一的实现是 `src/turn-cwd.mjs` 的 `resolveSessionReuse`。 // 父进程没给(老协议)时才退回自己判一次(保底,不是常规路径)。 + // ★ 父进程没给判定时(老协议 / 异常帧),**委托给唯一那份规则**判一次, + // 而不是就地再写一个谓词(pi 评审 2026-09-15 §三:那份谓词比唯一规则**松** —— + // 只看"文件在不在",不要求 `storedCwd`,于是"有会话文件但没有 cwd"这一支 + // 会得出与唯一规则不同的结论)。 + // + // 这条路径**当前不可达**:`workerLaunch` 只有一个调用者,而它无条件注入 + // `sessionReused`。但一旦有人新增第二个启动点,它立刻复活 —— + // 所以它必须有信号(下面那行 log),否则将来复活时唯一的证据是"行为不对"。 const decidedReused = job.session?.sessionReused; + if (decidedReused === undefined && given) { + log('复用判定:父进程没给结论(老协议?),按唯一规则 resolveSessionReuse 就地判一次'); + } const reuse = decidedReused === undefined - ? { reused: Boolean(given && existsSync(given)) } + ? resolveSessionReuse({ sessionFile: given, storedCwd: job.session?.cwd, exists: existsSync }) : { reused: Boolean(decidedReused) }; if (reuse.reused && given) { diff --git a/plugins/pi-mail-bridge/test/turn-cwd.test.mjs b/plugins/pi-mail-bridge/test/turn-cwd.test.mjs index e7b8912..185955e 100644 --- a/plugins/pi-mail-bridge/test/turn-cwd.test.mjs +++ b/plugins/pi-mail-bridge/test/turn-cwd.test.mjs @@ -147,6 +147,24 @@ test('★ 单点:复用判定只有一处实现,worker 不再自己 existsSy assert.match(worker, /const decidedReused = job\.session\?\.sessionReused/, 'worker 必须消费父进程的复用判定'); assert.match(worker, /decidedReused === undefined/, '并且只在这个判定缺失时才退回自己判'); + + // ★ ③ 这条是 pi 让我补的(评审 §三 最后一句),而我**第一次报告说补了、其实没补**: + // 那条"单点"判据当时只断言了"有没有消费父进程的结论", + // **抓不到** worker 里第二处判 `sessionFile` 的谓词 —— 名字替实现作了证。 + // + // ★ 判据必须**只看代码行**:worker 里 `existsSync` 出现在注释里(讲这件事的那段), + // 也出现在 `import` 行上,还出现在 `resolveSessionReuse({… exists: existsSync })` 这个 + // **合法**的位置。所以判据不能是"文件里出现 existsSync"—— + // 我上一版的自查就是这么误判的(grep 命中的是注释与判据名)。 + // 做法:剥掉注释行,再看 `existsSync(` **作为调用**出现在哪些行; + // 它只允许出现在"把 existsSync 交给唯一规则"的那一行上。 + const codeLines = worker.split('\n') + .filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)); + const callLines = codeLines.filter((l) => /existsSync\s*\(/.test(l)); + for (const l of callLines) { + assert.match(l, /resolveSessionReuse\(\{[^}]*exists:\s*existsSync\s*\)/, + `worker 里对 existsSync 的调用只允许出现在"交给唯一规则"那一行,实际:${l.trim()}`); + } }); test('resolveSessionReuse:文件在/不在、cwd 有无,四种组合', () => {