{"version":3,"file":"file-logical-agent-store.d.ts","sourceRoot":"","sources":["../../../src/core/shared-inference/file-logical-agent-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,EAEN,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAUrD,MAAM,WAAW,4BAA4B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAIhD;AAED,qBAAa,qBAAsB,YAAW,iBAAiB;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B,YAAY,OAAO,EAAE,4BAA4B,EAGhD;IAED,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;YAIF,WAAW;YAaX,OAAO;YAQP,YAAY;IAwCpB,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAalE;IAEK,IAAI,CACT,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAO,GACzC,OAAO,CAAC,sBAAsB,CAAC,CAwBjC;IAEK,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAe9B;CACD;AAED,wBAAgB,2BAA2B,CAAC,IAAI,GAAE,MAAkC,GAAG,qBAAqB,CAE3G","sourcesContent":["/**\n * File logical agent store (3.0.0 foundation).\n *\n * Per-agent files with atomic writes + per-agent cross-process file lock,\n * mirroring FileDurableMissionStore. Optimistic revision compare-save prevents\n * two control planes from silently overwriting a transition.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport {\n\tisSafeLogicalAgentId,\n\ttype LogicalAgentLoadResult,\n\ttype LogicalAgentSaveResult,\n\ttype LogicalAgentStore,\n\tparseLogicalAgentRecord,\n} from \"./logical-agent.js\";\nimport type { LogicalAgentRecord } from \"./types.js\";\n\nconst RECORD_SUFFIX = \".agent.json\";\nconst ATOMIC_SUFFIX = \".tmp\";\n\nconst DEFAULT_LOCK_STALE_MS = 30_000;\nconst DEFAULT_LOCK_RETRIES = 8;\nconst DEFAULT_LOCK_MIN_TIMEOUT_MS = 20;\nconst DEFAULT_LOCK_MAX_TIMEOUT_MS = 250;\n\nexport interface FileLogicalAgentStoreOptions {\n\troot: string;\n\tstoreId?: string;\n}\n\nexport function defaultLogicalAgentRoot(): string {\n\tconst env = process.env.JENSEN_LOGICAL_AGENT_DIR;\n\tif (env?.trim()) return env.trim();\n\treturn path.join(os.homedir(), \".jensen\", \"logical-agents\");\n}\n\nexport class FileLogicalAgentStore implements LogicalAgentStore {\n\treadonly storeId: string;\n\tprivate readonly root: string;\n\n\tconstructor(options: FileLogicalAgentStoreOptions) {\n\t\tthis.root = path.resolve(options.root);\n\t\tthis.storeId = options.storeId ?? \"file\";\n\t}\n\n\tprivate resolve(logicalAgentId: string): string {\n\t\treturn path.join(this.root, `${logicalAgentId}${RECORD_SUFFIX}`);\n\t}\n\n\tprivate assertId(logicalAgentId: string): void {\n\t\tif (!isSafeLogicalAgentId(logicalAgentId)) throw new Error(`Unsafe logical agent id: ${logicalAgentId}`);\n\t}\n\n\tprivate async writeAtomic(target: string, content: string): Promise<void> {\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst tmp = `${target}.${randomUUID()}${ATOMIC_SUFFIX}`;\n\t\tconst fh = await fsp.open(tmp, \"w\");\n\t\ttry {\n\t\t\tawait fh.writeFile(content, \"utf8\");\n\t\t\tawait fh.sync();\n\t\t} finally {\n\t\t\tawait fh.close();\n\t\t}\n\t\tawait fsp.rename(tmp, target);\n\t}\n\n\tprivate async readRaw(logicalAgentId: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\treturn await fsp.readFile(this.resolve(logicalAgentId), \"utf8\");\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async withFileLock<T>(logicalAgentId: string, fn: () => Promise<T>): Promise<T> {\n\t\tthis.assertId(logicalAgentId);\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst target = this.resolve(logicalAgentId);\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\ttry {\n\t\t\trelease = await lockfile.lock(target, {\n\t\t\t\trealpath: false,\n\t\t\t\tstale: DEFAULT_LOCK_STALE_MS,\n\t\t\t\tretries: {\n\t\t\t\t\tretries: DEFAULT_LOCK_RETRIES,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: DEFAULT_LOCK_MIN_TIMEOUT_MS,\n\t\t\t\t\tmaxTimeout: DEFAULT_LOCK_MAX_TIMEOUT_MS,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t? (error as { code?: unknown }).code\n\t\t\t\t\t: undefined;\n\t\t\tif (code === \"ELOCKED\") throw new Error(`Timed out acquiring logical agent lock for ${logicalAgentId}`);\n\t\t\tif (code === \"ENOTEMPTY\" || code === \"ENOTDIR\")\n\t\t\t\tthrow new Error(`Corrupt logical agent lock metadata for ${logicalAgentId}`);\n\t\t\tthrow error;\n\t\t}\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Best-effort release.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync load(logicalAgentId: string): Promise<LogicalAgentLoadResult> {\n\t\tthis.assertId(logicalAgentId);\n\t\tconst raw = await this.readRaw(logicalAgentId);\n\t\tif (raw === undefined) return { status: \"missing\" };\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(raw);\n\t\t} catch {\n\t\t\treturn { status: \"corrupt\", logicalAgentId, diagnostic: \"record is not valid JSON\" };\n\t\t}\n\t\tconst result = parseLogicalAgentRecord(parsed);\n\t\tif (!result.ok) return { status: \"corrupt\", logicalAgentId, diagnostic: result.diagnostic };\n\t\treturn { status: \"ok\", record: result.record };\n\t}\n\n\tasync save(\n\t\trecord: LogicalAgentRecord,\n\t\toptions: { expectedRevision?: number } = {},\n\t): Promise<LogicalAgentSaveResult> {\n\t\tthis.assertId(record.logicalAgentId);\n\t\treturn this.withFileLock(record.logicalAgentId, async () => {\n\t\t\tif (options.expectedRevision !== undefined) {\n\t\t\t\tconst current = await this.load(record.logicalAgentId);\n\t\t\t\tif (current.status !== \"ok\") {\n\t\t\t\t\treturn { status: \"stale\", expectedRevision: options.expectedRevision, actualRevision: undefined };\n\t\t\t\t}\n\t\t\t\tif (current.record.revision !== options.expectedRevision) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"stale\",\n\t\t\t\t\t\texpectedRevision: options.expectedRevision,\n\t\t\t\t\t\tactualRevision: current.record.revision,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst next: LogicalAgentRecord = {\n\t\t\t\t...record,\n\t\t\t\tupdatedAtMs: Date.now(),\n\t\t\t\trevision: record.revision + 1,\n\t\t\t};\n\t\t\tawait this.writeAtomic(this.resolve(record.logicalAgentId), JSON.stringify(next, null, 2));\n\t\t\treturn { status: \"saved\", record: next };\n\t\t});\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await fsp.readdir(this.root);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tconst ids: string[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.endsWith(ATOMIC_SUFFIX)) continue;\n\t\t\tif (!entry.endsWith(RECORD_SUFFIX)) continue;\n\t\t\tconst id = entry.slice(0, -RECORD_SUFFIX.length);\n\t\t\tif (isSafeLogicalAgentId(id)) ids.push(id);\n\t\t}\n\t\treturn ids.sort();\n\t}\n}\n\nexport function createFileLogicalAgentStore(root: string = defaultLogicalAgentRoot()): FileLogicalAgentStore {\n\treturn new FileLogicalAgentStore({ root });\n}\n"]}