/** * 短生命周期打开 AgentFS:回调结束即 close,不进入 sandbox host 缓存。 * * 适用: * - 无 live host 时的只读 browse * - checkpoint 库(无 live host) * - 已在 HostRegistry.withExclusiveAccess 保护下的 session 库读写 * * session 库优先走 withSessionFileSystem:复用 live host,禁止与 host 双开。 * 禁止:在 live host 可能仍占用 session .db 时,绕过 exclusive/shared 直接 * withAgentFsAtPath(sessionDb) 做 create/restore 等大范围改写。 */ import { mkdir, stat } from "node:fs/promises"; import { dirname } from "node:path"; import { AgentFS } from "agentfs-sdk"; import type { FileSystem } from "agentfs-sdk"; import { hostRegistry } from "../agentfs-sandbox/host.js"; import { AgentFsDebugError, mapAgentFsOpenError } from "./errors.js"; import { resolveSessionDbPath } from "./paths.js"; export type OpenAgent = Awaited>; async function closeQuietly(agent: OpenAgent): Promise { try { await agent.close(); } catch { // 关闭失败不影响主结果 } } /** 确认磁盘上的 .db 已存在,避免 AgentFS.open 对缺失路径静默建空库。 */ export async function assertDbExists( dbPath: string, notFound: { code: string; message: string }, ): Promise { try { await stat(dbPath); } catch (err) { const code = (err as NodeJS.ErrnoException).code; if (code === "ENOENT") { throw new AgentFsDebugError(404, notFound.code, notFound.message); } throw err; } } /** 确认 session .db 已存在。 */ export async function assertSessionDbExists(sessionId: string): Promise { const sessionDb = resolveSessionDbPath(sessionId); await assertDbExists(sessionDb, { code: "session_not_found", message: `session 不存在: ${sessionId}`, }); return sessionDb; } /** * 在指定 db 路径上打开 AgentFS,执行回调后关闭。 * @param mustExist 为 true 时禁止 open 静默创建空库;为 false 时会 mkdir 父目录后 open(checkpoint 新建)。 * @param notFound mustExist 时 404 的 code/message;默认 session_not_found。 */ export async function withAgentFsAtPath( dbPath: string, fn: (agent: OpenAgent) => Promise, opts?: { mustExist?: boolean; label?: string; notFound?: { code: string; message: string }; }, ): Promise { const mustExist = opts?.mustExist ?? true; const label = opts?.label ?? dbPath; if (mustExist) { await assertDbExists( dbPath, opts?.notFound ?? { code: "session_not_found", message: `库不存在: ${label}`, }, ); } else { await mkdir(dirname(dbPath), { recursive: true }); } let agent: OpenAgent; try { agent = await AgentFS.open({ path: dbPath }); } catch (err) { throw mapAgentFsOpenError(err, label); } try { return await fn(agent); } finally { await closeQuietly(agent); } } /** * 访问 session 虚拟 FS:优先复用 HostRegistry live handle(不二次 open SQLite); * 无 live 时在 shared 串行锁内短生命周期 open。 * * 与 exclusive / getOrOpen 同路径串行,避免 browse/create 与 restore 竞态双开。 */ export async function withSessionFileSystem( sessionId: string, fn: (fs: FileSystem) => Promise, ): Promise { const dbPath = await assertSessionDbExists(sessionId); return hostRegistry.withSharedAccess(dbPath, async () => { const live = hostRegistry.getByDbPath(dbPath); if (live) { return fn(live.agentFs.fs); } return withAgentFsAtPath( dbPath, (agent) => fn(agent.fs), { mustExist: true, label: sessionId, notFound: { code: "session_not_found", message: `session 不存在: ${sessionId}`, }, }, ); }); } /** * @deprecated 优先 withSessionFileSystem,避免与 live host 双开。 * 保留给确知无 live、或已在 exclusive 内的调用方。 */ export async function withAgentFs( sessionId: string, fn: (agent: OpenAgent) => Promise, ): Promise { const dbPath = resolveSessionDbPath(sessionId); return withAgentFsAtPath(dbPath, fn, { mustExist: true, label: sessionId, notFound: { code: "session_not_found", message: `session 不存在: ${sessionId}`, }, }); }