/** * 决定"**这一轮 worker 用哪个 cwd**"—— 父子两侧的唯一来源。 * * # 为什么需要这个模块(一次实测出来的分叉) * * worker 的沙箱 `--rw` 清单由**父进程**算(`pool.mjs` → `lib/sandbox.js`), * 而 worker 实际用的 cwd 是它自己推的。两个来源一旦不一致,症状是 * **"界内也写不了"** —— 最难查的一类,因为 EACCES 落在"界内", * 读日志的人会以为沙箱装错了。 * * `pool.mjs` 里那句注释原本防的是**公式**分叉(父子都调 `resolveWorkspaceCwd`)。 * 但 cwd 有**两个来源**,所以只统一公式不够(pi 评审 2026-09-15 用探针实测): * * 父进程:`resolveWorkspaceCwd(to_workspace, …)` ← 来源:**这封信的地址** * 子进程:`job.session.cwd || resolveWorkspaceCwd(…)` ← 来源:**这个会话上次实际用的 cwd** * * 而会话键 `keyOf(data) = data.session_id` —— **只看 session_id,不看 path 位**。 * 于是同一个 session_id 下地址换个形状(`pi@/some/dir` → `pi@.<会话>`, * 或换成另一个已存在目录),父进程按**新地址**算 rw,worker 却**复用会话、 * 落在旧 cwd** 上 ⇒ rw 里没有 worker 真正要写的那个目录。 * * 实测(探针,`exists` 注入): * * 父进程算出的 cwd = /root/.pi/mail-sessions/sess-x * worker 实际会用 = /home/program/agentmail (= 这个会话的 state.cwd) * sandboxed = true, rw 含 worker 实际 cwd? = **false** * * 不是假想:本线程那条会话自上线起 5 次启动的 rw 全是 `/home/program/agentmail`, * 那就是它的 `state.cwd`;此时只要来一封写给 `pi@.<会话>`(不带 path)的信就会踩到。 * * # 这条与"首回合补目录"那次修复的关系(必须一起看) * * `pool.mjs` 先 `ensureCwd` 再算 launch 的修复(commit `99e6560`)在**首回合**这个目标上 * 是净胜,但它在"地址换形状"这个组合上**改变了失败方式**: * * · 修复前:兜底目录不存在 ⇒ 不套沙箱 ⇒ `ask`(有人应答时**写得进去**) * · 修复后:目录被建出来 ⇒ **套上沙箱,而 rw 是地址算的那个** ⇒ worker 在会话自己的 * cwd 里写 ⇒ **EACCES,且没有"问一次"这条路**(内核拒的,不是策略问的) * * ⇒ 两处必须一起对齐,否则第一处会把一个"多问一次"变成"界内也写不了"。 * * # 残余(已知,未修) * * **接管会话**(`adoptID`)那条路上,worker 用的是**会话文件 header 里的** `info.cwd` * (`worker.mjs` 的接管分支),父进程读不到 ⇒ 仍会错位。父进程要用 `session-scan.mjs` * 才能拿到它,而那个模块的 `readHeader` 未导出、整表 `scan()` 在父进程里代价大 * (worker 里实测 1431ms / 240MB)。彻底的方向是**把"这次用哪个 cwd"收成父进程一处决定** * (它有 `state.sessionFile`,header 也能读),worker 只消费、不再自己推导 —— 三来源变一来源。 * 在那之前,接管路径的错位没有任何一层提示,已记入 `docs/DEBTS.json`。 */ /** * 这一轮该用哪个 cwd。 * * 输入**全部来自 public 状态**(父进程也拿得到),所以两侧算出的一定是同一个值。 * * @param {object} o * @param {boolean} o.sessionReused 这次是否复用已有会话(= `job.session.sessionFile` 存在且文件在) * @param {string} [o.storedCwd] 复用时这个会话**上次实际用的** cwd(`state.cwd`) * @param {string} [o.toWorkspace] 这封信地址里的 path 位 * @param {string|number} [o.sessionKey] 会话标识(兜底目录用) * @param {(key: string|number|undefined) => string} o.fallback 平台兜底目录函数 * @param {(workspace: string|undefined, fallback: string) => {cwd: string, grouped: boolean}} o.resolve * `resolveWorkspaceCwd` —— 注入是为了可测且不重复实现寻址规则 * @returns {{cwd: string, grouped: boolean, source: 'reused'|'address'}} * `source` 是给日志/判据用的:能一眼看出这一轮 cwd 是**复用**来的还是**按地址**算的 */ export function resolveTurnCwd({ sessionReused, storedCwd, toWorkspace, sessionKey, fallback, resolve, }) { // 与 worker 同一条规则:复用会话时,**上次实际用的 cwd 优先**。 // 这正是父进程原先漏掉的那一支 —— 父进程只看了地址。 if (sessionReused && storedCwd) { return { cwd: String(storedCwd), grouped: true, source: 'reused' }; } const { cwd, grouped } = resolve(toWorkspace, fallback(sessionKey)); return { cwd, grouped, source: 'address' }; }