{"version":3,"file":"file-executor-registry.d.ts","sourceRoot":"","sources":["../../../src/core/executor-registry/file-executor-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAG1B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,KAAK,cAAc,EAA2C,MAAM,8BAA8B,CAAC;AAU5G,MAAM,WAAW,2BAA2B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAIpD;AAED,qBAAa,oBAAqB,YAAW,qBAAqB;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,YAAY,OAAO,EAAE,2BAA2B,EAO/C;IAED,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,gBAAgB;YAMV,WAAW;YAaX,OAAO;YAQP,YAAY;YAgEZ,UAAU;IAoBlB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAmCtE;IAEK,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgB1D;IAEK,MAAM,CAAC,CAAC,EACb,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,gBAAgB,CAAC,CAAC,CAAC,GACxD,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAqBlC;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAevC;CACD;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,MAAsC,GAAG,oBAAoB,CAE7G","sourcesContent":["/**\n * File Executor Registry (2.10.0).\n *\n * Local durable implementation of the ExecutorRegistryStore port. Records are\n * written atomically (unique temp + fsync + rename) and schema-validated on\n * load. Each executor record is independently locked cross-process with\n * proper-lockfile, so unrelated executor ids can mutate concurrently.\n *\n * Concurrency model mirrors FileDurableMissionStore: the lock protects only the\n * short read-validate-write critical section, never held across heartbeat\n * cadences, model inference, or resource collection.\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\ttype ExecutorLoadResult,\n\ttype ExecutorMutateResult,\n\ttype ExecutorMutation,\n\ttype ExecutorRegisterResult,\n\ttype ExecutorRegistryStore,\n\texecutorDefinitionsEqual,\n\tparseExecutorRecord,\n} from \"./executor-registry-store.js\";\nimport { type ExecutorRecord, ExecutorRegistryError, isSafeExecutorId } from \"./executor-registry-types.js\";\n\nconst RECORD_SUFFIX = \".executor.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 FileExecutorRegistryOptions {\n\troot: string;\n\tstoreId?: string;\n\tlockStaleMs?: number;\n\tlockRetries?: number;\n\tlockMinTimeoutMs?: number;\n\tlockMaxTimeoutMs?: number;\n}\n\nexport function defaultExecutorRegistryRoot(): string {\n\tconst env = process.env.JENSEN_EXECUTOR_REGISTRY_DIR;\n\tif (env?.trim()) return env.trim();\n\treturn path.join(os.homedir(), \".jensen\", \"agent\", \"executor-registry\");\n}\n\nexport class FileExecutorRegistry implements ExecutorRegistryStore {\n\treadonly storeId: string;\n\tprivate readonly root: string;\n\tprivate readonly lockStaleMs: number;\n\tprivate readonly lockRetries: number;\n\tprivate readonly lockMinTimeoutMs: number;\n\tprivate readonly lockMaxTimeoutMs: number;\n\n\tconstructor(options: FileExecutorRegistryOptions) {\n\t\tthis.root = path.resolve(options.root);\n\t\tthis.storeId = options.storeId ?? \"file\";\n\t\tthis.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;\n\t\tthis.lockRetries = options.lockRetries ?? DEFAULT_LOCK_RETRIES;\n\t\tthis.lockMinTimeoutMs = options.lockMinTimeoutMs ?? DEFAULT_LOCK_MIN_TIMEOUT_MS;\n\t\tthis.lockMaxTimeoutMs = options.lockMaxTimeoutMs ?? DEFAULT_LOCK_MAX_TIMEOUT_MS;\n\t}\n\n\tprivate resolve(executorId: string): string {\n\t\treturn path.join(this.root, `${executorId}${RECORD_SUFFIX}`);\n\t}\n\n\tprivate assertExecutorId(executorId: string): void {\n\t\tif (!isSafeExecutorId(executorId)) {\n\t\t\tthrow new ExecutorRegistryError(\"INVALID_EXECUTOR_ID\", `Unsafe executor id: ${executorId}`, { executorId });\n\t\t}\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\tawait fsp.writeFile(tmp, content, \"utf8\");\n\t\tconst fh = await fsp.open(tmp, \"r\");\n\t\ttry {\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(executorId: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\treturn await fsp.readFile(this.resolve(executorId), \"utf8\");\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async withFileLock<T>(executorId: string, fn: () => Promise<T>): Promise<T> {\n\t\tthis.assertExecutorId(executorId);\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst target = this.resolve(executorId);\n\n\t\tlet compromised = false;\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: this.lockStaleMs,\n\t\t\t\tretries: {\n\t\t\t\t\tretries: this.lockRetries,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: this.lockMinTimeoutMs,\n\t\t\t\t\tmaxTimeout: this.lockMaxTimeoutMs,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tonCompromised: () => {\n\t\t\t\t\tcompromised = 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\") {\n\t\t\t\tthrow new ExecutorRegistryError(\n\t\t\t\t\t\"REGISTRY_LOCK_TIMEOUT\",\n\t\t\t\t\t`Timed out acquiring registry mutation lock for executor ${executorId}`,\n\t\t\t\t\t{ executorId },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (code === \"ENOTEMPTY\" || code === \"ENOTDIR\") {\n\t\t\t\tthrow new ExecutorRegistryError(\n\t\t\t\t\t\"EXECUTOR_CORRUPT\",\n\t\t\t\t\t`Corrupt registry mutation lock metadata for executor ${executorId}`,\n\t\t\t\t\t{ executorId },\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\ttry {\n\t\t\tif (compromised) {\n\t\t\t\tthrow new ExecutorRegistryError(\n\t\t\t\t\t\"EXECUTOR_CORRUPT\",\n\t\t\t\t\t`Registry mutation lock for executor ${executorId} was compromised`,\n\t\t\t\t\t{ executorId },\n\t\t\t\t);\n\t\t\t}\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; a successful mutation must never become an error.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async readParsed(\n\t\texecutorId: string,\n\t): Promise<{ status: \"ok\"; record: ExecutorRecord } | { status: \"missing\" } | { status: \"corrupt\" }> {\n\t\tconst raw = await this.readRaw(executorId);\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\" };\n\t\t}\n\t\tconst result = parseExecutorRecord(parsed);\n\t\tif (!result.ok) return { status: \"corrupt\" };\n\t\treturn { status: \"ok\", record: result.record };\n\t}\n\n\t// =========================================================================\n\t// ExecutorRegistryStore\n\t// =========================================================================\n\n\tasync register(record: ExecutorRecord): Promise<ExecutorRegisterResult> {\n\t\tthis.assertExecutorId(record.executorId);\n\n\t\treturn this.withFileLock(record.executorId, async () => {\n\t\t\tconst existing = await this.load(record.executorId);\n\t\t\tif (existing.status === \"ok\") {\n\t\t\t\tif (\n\t\t\t\t\texecutorDefinitionsEqual(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\texecutorId: existing.record.executorId,\n\t\t\t\t\t\t\tdisplayName: existing.record.displayName,\n\t\t\t\t\t\t\tlabels: existing.record.labels,\n\t\t\t\t\t\t\tconfiguredCapabilities: existing.record.configuredCapabilities,\n\t\t\t\t\t\t\tremoteTargetId: existing.record.remoteTargetId,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\texecutorId: record.executorId,\n\t\t\t\t\t\t\tdisplayName: record.displayName,\n\t\t\t\t\t\t\tlabels: record.labels,\n\t\t\t\t\t\t\tconfiguredCapabilities: record.configuredCapabilities,\n\t\t\t\t\t\t\tremoteTargetId: record.remoteTargetId,\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn { status: \"idempotent\", record: existing.record };\n\t\t\t\t}\n\t\t\t\treturn { status: \"conflict\", error: \"executorId already registered with a different definition\" };\n\t\t\t}\n\t\t\tif (existing.status === \"corrupt\") {\n\t\t\t\treturn { status: \"conflict\", error: `existing executor record is corrupt: ${existing.diagnostic}` };\n\t\t\t}\n\n\t\t\tawait this.writeAtomic(this.resolve(record.executorId), JSON.stringify(record, null, 2));\n\t\t\treturn { status: \"created\" };\n\t\t});\n\t}\n\n\tasync load(executorId: string): Promise<ExecutorLoadResult> {\n\t\tthis.assertExecutorId(executorId);\n\t\tconst raw = await this.readRaw(executorId);\n\t\tif (raw === undefined) return { status: \"missing\" };\n\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\", executorId, diagnostic: \"record is not valid JSON\" };\n\t\t}\n\t\tconst result = parseExecutorRecord(parsed);\n\t\tif (!result.ok) {\n\t\t\treturn { status: \"corrupt\", executorId, diagnostic: result.diagnostic };\n\t\t}\n\t\treturn { status: \"ok\", record: result.record };\n\t}\n\n\tasync mutate<T>(\n\t\texecutorId: string,\n\t\tmutation: (current: ExecutorRecord) => ExecutorMutation<T>,\n\t): Promise<ExecutorMutateResult<T>> {\n\t\tthis.assertExecutorId(executorId);\n\n\t\treturn this.withFileLock(executorId, async () => {\n\t\t\tconst current = await this.readParsed(executorId);\n\t\t\tif (current.status === \"missing\") return { status: \"missing\" };\n\t\t\tif (current.status === \"corrupt\") {\n\t\t\t\treturn { status: \"corrupt\", executorId, diagnostic: \"record is not valid or schema-invalid\" };\n\t\t\t}\n\n\t\t\tconst output = mutation(current.record);\n\t\t\tif (output.kind === \"noop\") return { status: \"ok\", value: output.value };\n\n\t\t\tconst nextValidation = parseExecutorRecord(output.next);\n\t\t\tif (!nextValidation.ok) {\n\t\t\t\tthrow new Error(`Mutation produced an invalid record for ${executorId}: ${nextValidation.diagnostic}`);\n\t\t\t}\n\n\t\t\tawait this.writeAtomic(this.resolve(executorId), JSON.stringify(output.next, null, 2));\n\t\t\treturn { status: \"ok\", value: output.value };\n\t\t});\n\t}\n\n\tasync listExecutors(): 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 (isSafeExecutorId(id)) ids.push(id);\n\t\t}\n\t\treturn ids.sort();\n\t}\n}\n\n/** Convenience factory using the default Jensen executor registry directory. */\nexport function createFileExecutorRegistry(root: string = defaultExecutorRegistryRoot()): FileExecutorRegistry {\n\treturn new FileExecutorRegistry({ root });\n}\n"]}