{"version":3,"file":"process-registry.mjs","names":[],"sources":["../src/process-registry.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Effect } from \"effect\";\n\nexport type ProcessRole = \"standalone\" | \"workspace-parent\" | \"workspace-child\";\n\nexport interface PidEntry {\n  pid: number;\n  configDir: string;\n  parentPid?: number;\n  role: ProcessRole;\n  ports: Record<string, number>;\n  childPids?: number[];\n  budget?: { min: number; max: number };\n  // Lease seam (ADR 0012 §6): entries are leases on shared resources, keyed\n  // by what they hold; refcount-ready for the shared-plugin broker.\n  leaseKey?: string;\n  refcount?: number;\n  startedAt: number;\n  // Identity of the claimed pid captured at registration time (ADR 0012\n  // amendment): PIDs are reused across container restarts, so a bare pid\n  // cannot prove a claim live. `processStartTime` is stable per process\n  // generation — a mismatch proves the recorded session is dead.\n  processStart?: string;\n  description: string;\n}\n\nfunction getRegistryDir(): string {\n  return join(homedir(), \".cache\", \"everything-dev\");\n}\n\nfunction ensureRegistryDir(): void {\n  const path = getRegistryPath();\n  const dir = join(path, \"..\");\n  if (!existsSync(dir)) {\n    mkdirSync(dir, { recursive: true });\n  }\n}\n\nexport function readRegistry(): PidEntry[] {\n  const path = getRegistryPath();\n  if (!existsSync(path)) return [];\n  try {\n    const raw = JSON.parse(readFileSync(path, \"utf-8\"));\n    if (!Array.isArray(raw)) return [];\n    return raw.filter(\n      (entry): entry is PidEntry =>\n        entry &&\n        typeof entry === \"object\" &&\n        typeof entry.pid === \"number\" &&\n        typeof entry.configDir === \"string\" &&\n        typeof entry.role === \"string\",\n    );\n  } catch {\n    return [];\n  }\n}\n\nexport function isPidAlive(pid: number): boolean {\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch (err) {\n    return (err as NodeJS.ErrnoException).code !== \"ESRCH\";\n  }\n}\n\n/**\n * Identity of a process generation, stable across PID reuse: Linux reads the\n * kernel's starttime tick from /proc, macOS falls back to `ps -o lstart=`.\n * Null when the generation cannot be determined (the caller then treats the\n * claim as unverifiable and keeps legacy behavior).\n */\nexport function processStartTime(pid: number): string | null {\n  if (pid <= 1) return null;\n  try {\n    const stat = readFileSync(`/proc/${pid}/stat`, \"utf-8\");\n    const close = stat.lastIndexOf(\")\");\n    const fields = stat.slice(close + 2).split(\" \");\n    const starttime = fields[19];\n    return starttime ? `proc:${starttime}` : null;\n  } catch {\n    try {\n      const out = spawnSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], { timeout: 5000 });\n      const text = out.stdout?.toString().trim();\n      return text ? `ps:${text}` : null;\n    } catch {\n      return null;\n    }\n  }\n}\n\nexport function isEntryLive(entry: PidEntry): boolean {\n  if (entry.pid <= 1) return false;\n  if (!existsSync(entry.configDir)) return false;\n  if (!isPidAlive(entry.pid)) return false;\n  if (entry.processStart !== undefined) {\n    return processStartTime(entry.pid) === entry.processStart;\n  }\n  return true;\n}\n\nexport function pruneDead(entries: PidEntry[]): PidEntry[] {\n  return entries.filter(isEntryLive);\n}\n\nexport function pruneDeadEffect(entries: PidEntry[]): Effect.Effect<PidEntry[]> {\n  return Effect.forEach(\n    entries,\n    (entry) => {\n      if (entry.pid <= 1) return Effect.succeed(null);\n      return Effect.gen(function* () {\n        const dirExists = yield* Effect.promise(() =>\n          access(entry.configDir)\n            .then(() => true)\n            .catch(() => false),\n        );\n        if (!dirExists) return null;\n        if (!isPidAlive(entry.pid)) return null;\n        if (entry.processStart !== undefined) {\n          const current = yield* Effect.promise(() => Promise.resolve(processStartTime(entry.pid)));\n          if (current !== entry.processStart) return null;\n        }\n        return entry;\n      });\n    },\n    { concurrency: \"unbounded\" },\n  ).pipe(Effect.map((results) => results.filter((e): e is PidEntry => e !== null)));\n}\n\nexport function writeRegistry(entries: PidEntry[]): void {\n  ensureRegistryDir();\n  const path = getRegistryPath();\n  const tmpPath = `${path}.tmp`;\n  writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}\\n`);\n  renameSync(tmpPath, path);\n}\n\nexport function registerStandalone(entry: Omit<PidEntry, \"role\">): PidEntry {\n  const live = pruneDead(readRegistry());\n  const full: PidEntry = {\n    ...entry,\n    role: \"standalone\",\n    processStart: processStartTime(entry.pid) ?? entry.processStart,\n  };\n  const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n  withoutSelf.push(full);\n  writeRegistry(withoutSelf);\n  return full;\n}\n\nexport function registerEntry(entry: PidEntry): void {\n  const live = pruneDead(readRegistry());\n  const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n  withoutSelf.push({\n    ...entry,\n    processStart: processStartTime(entry.pid) ?? entry.processStart,\n  });\n  writeRegistry(withoutSelf);\n}\n\nexport function updateChildPids(pid: number, childPids: number[]): void {\n  const live = pruneDead(readRegistry());\n  const entry = live.find((existing) => existing.pid === pid);\n  if (!entry) return;\n  entry.childPids = childPids;\n  writeRegistry(live);\n}\n\nexport function unregisterPid(pid: number): void {\n  const entries = readRegistry();\n  const next = pruneDead(entries).filter((entry) => entry.pid !== pid);\n  if (next.length === entries.length) return;\n  writeRegistry(next);\n}\n\nexport function removeRegistryFile(): void {\n  const path = getRegistryPath();\n  if (existsSync(path)) {\n    rmSync(path, { force: true });\n  }\n}\n\nexport function getRegistryPath(): string {\n  return process.env.BO_PID_REGISTRY_PATH ?? join(getRegistryDir(), \"pids.json\");\n}\n\nexport function claimedPorts(): Set<number> {\n  const out = new Set<number>(claimedPortOwners().keys());\n  return out;\n}\n\n/** Port → the live registry entry claiming it, for occupancy diagnostics. */\nexport function claimedPortOwners(): Map<number, PidEntry> {\n  const live = pruneDead(readRegistry());\n  const out = new Map<number, PidEntry>();\n  for (const entry of live) {\n    for (const port of Object.values(entry.ports)) {\n      if (typeof port === \"number\") out.set(port, entry);\n    }\n  }\n  return out;\n}\n"],"mappings":"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"}