{"version":3,"file":"harnessStore-BtWteJvc.cjs","names":["fs","readHarnessState","HARNESS_STATE_KEYS","path","os"],"sources":["../src/models/harnessCache.ts","../src/services/harnessStore/index.ts"],"sourcesContent":["import type { HarnessState } from '../types/config';\n\nexport interface LoadedHarnessState {\n  /** Undefined when this process is running off the environment fallback. */\n  filePath?: string;\n  owned: boolean;\n  state: HarnessState;\n}\n\nexport interface CachedHarnessState extends LoadedHarnessState {\n  /** The file's mtime when it was read, so a rewrite by another copy is noticed. */\n  mtimeMs?: number;\n}\n\nexport const harnessCache: { loaded: CachedHarnessState | undefined } = { loaded: undefined };\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { harnessCache, type CachedHarnessState, type LoadedHarnessState } from '../../models/harnessCache';\nimport type { HarnessState } from '../../types/config';\nimport { writePrivateAtomicJson } from '../atomicJson';\nimport {\n  HARNESS_STATE_KEYS,\n  projectHarnessEnvironment,\n  readHarnessState,\n  type HarnessStateParseReporter,\n} from '../harnessState';\n\n/**\n * Who owns the harness state, and where it lives.\n *\n * The state used to be the environment: twenty variables that every reader\n * re-parsed and every writer mutated globally. The file is the authority now,\n * and the environment is a projection published for the readers that can only\n * see an environment, which are bash hooks, the shell launchers, packages that\n * do not depend on this one, and any process spawned by any of them.\n *\n * OWNERSHIP IS PER PROCESS, AND THAT IS THE WHOLE DESIGN:\n * Doom Team subagents run detached, in their own process group, from an\n * environment snapshot taken at spawn. A child can outlive its parent. If it\n * shared the parent's file it would corrupt a session it does not own, and if\n * that file sat in the parent's run directory the parent's shutdown sweep would\n * delete it from under a live run. So every process writes a file it owns:\n * `owner.pid` records who, and a process that finds someone else's file copies\n * it before its first write.\n *\n * AVOID:\n * - Treating the file as shared mutable state between processes. Atomic writes\n *   prevent torn reads, not lost updates\n * - Reading the environment directly for anything this exposes\n */\n\n/** Points at the state file. The one variable that is not derived. */\nexport const HARNESS_STATE_POINTER = 'DOOMPI_STATE';\n\nconst STATE_FILE_VERSION = 1;\nconst STATE_FILE_NAME = 'harness-state.json';\nconst ADOPTED_DIRECTORY = 'doom-harness';\n/**\n * Written for a process that does not exist yet, so the first one to read it\n * owns it. A spawner cannot know its child's pid, and a child that inherited\n * its parent's file would lose it the moment the parent cleaned up.\n */\nconst UNCLAIMED_OWNER = 0;\n\nexport interface HarnessStateFile {\n  version: number;\n  owner: { pid: number; startedAt: string };\n  state: HarnessState;\n}\n\nexport interface HarnessStateTransactionSnapshot {\n  readonly filePath?: string;\n  readonly owned: boolean;\n  readonly state: HarnessState;\n  readonly environment: Readonly<Record<string, string | undefined>>;\n}\n\n/** Drops the cached state. For tests, and for a process changing identity. */\nexport function resetHarnessStore(): void {\n  harnessCache.loaded = undefined;\n}\n\nfunction fileMtime(filePath: string): number | undefined {\n  try {\n    return fs.statSync(filePath).mtimeMs;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Whether the cache still describes what the process points at.\n *\n * A session can load more than one copy of this package (a layer package\n * installed under .pi/npm brings its own), and each copy caches on its own.\n * The pointer and the file are shared, so a switch persisted through one copy\n * moves the pointer or rewrites the file, and every other copy must notice\n * rather than answer from memory.\n */\nfunction cacheCurrent(cache: CachedHarnessState, environment: NodeJS.ProcessEnv): boolean {\n  const pointer = environment[HARNESS_STATE_POINTER];\n  if (cache.filePath !== pointer) return false;\n  if (!cache.filePath) return true;\n  try {\n    return fs.statSync(cache.filePath).mtimeMs === cache.mtimeMs;\n  } catch {\n    return false;\n  }\n}\n\nfunction parseStateFile(source: string, filePath: string): HarnessStateFile {\n  const parsed = JSON.parse(source) as Partial<HarnessStateFile>;\n  if (parsed.version !== STATE_FILE_VERSION) {\n    throw new Error(\n      `Harness state at ${filePath} has version ${String(parsed.version)}, expected ${STATE_FILE_VERSION}`,\n    );\n  }\n  if (typeof parsed.state !== 'object' || parsed.state === null) {\n    throw new Error(`Harness state at ${filePath} has no state object`);\n  }\n  return {\n    version: STATE_FILE_VERSION,\n    owner: { pid: parsed.owner?.pid ?? -1, startedAt: parsed.owner?.startedAt ?? '' },\n    state: parsed.state as HarnessState,\n  };\n}\n\n/**\n * Reads the state for this process, once.\n *\n * A missing pointer means nobody wrote a file for this process, so the\n * environment is all there is: a nested run, a third-party spawn, or a caller\n * older than the store. A pointer that cannot be read is reported and then\n * treated the same way, because a session that starts degraded beats a session\n * that does not start at all.\n */\nexport function loadHarnessState(\n  environment: NodeJS.ProcessEnv = process.env,\n  report?: HarnessStateParseReporter,\n): LoadedHarnessState {\n  // The cache describes this process. Asking about another environment is a\n  // question about another process, so it reads through rather than answering\n  // from what this one happens to hold.\n  const cacheable = environment === process.env;\n  if (cacheable && harnessCache.loaded && cacheCurrent(harnessCache.loaded, environment)) return harnessCache.loaded;\n\n  const filePath = environment[HARNESS_STATE_POINTER];\n  let result: CachedHarnessState | undefined;\n  if (filePath && fs.existsSync(filePath)) {\n    try {\n      const file = parseStateFile(fs.readFileSync(filePath, 'utf8'), filePath);\n      const owned = file.owner.pid === process.pid || file.owner.pid === UNCLAIMED_OWNER;\n      result = { filePath, owned, state: file.state, mtimeMs: fileMtime(filePath) };\n    } catch (error) {\n      report?.(HARNESS_STATE_POINTER, error);\n    }\n  }\n  result ??= { owned: false, state: readHarnessState(environment, report) };\n  if (cacheable) harnessCache.loaded = result;\n  return result;\n}\n\n/** Captures the owned state pointer and every projected harness variable. */\nexport function snapshotHarnessState(environment: NodeJS.ProcessEnv = process.env): HarnessStateTransactionSnapshot {\n  const loadedState = loadHarnessState(environment);\n  const environmentSnapshot: Record<string, string | undefined> = {\n    [HARNESS_STATE_POINTER]: environment[HARNESS_STATE_POINTER],\n  };\n  for (const key of Object.values(HARNESS_STATE_KEYS)) environmentSnapshot[key] = environment[key];\n  return {\n    filePath: loadedState.filePath,\n    owned: loadedState.owned,\n    state: structuredClone(loadedState.state),\n    environment: environmentSnapshot,\n  };\n}\n\n/** Restores a snapshot only while the current process still owns its state pointer. */\nexport function restoreHarnessStateSnapshot(\n  snapshot: HarnessStateTransactionSnapshot,\n  environment: NodeJS.ProcessEnv = process.env,\n): void {\n  const current = loadHarnessState(environment);\n  if (snapshot.filePath && snapshot.owned) {\n    if (!current.owned || current.filePath !== snapshot.filePath) {\n      throw new Error('Harness state ownership changed; refusing to restore an older snapshot.');\n    }\n    persist(snapshot.filePath, snapshot.state);\n  } else if (!snapshot.filePath) {\n    if (current.filePath && !current.owned) {\n      throw new Error('Harness state is no longer owned by this process; refusing to restore an older snapshot.');\n    }\n    if (current.filePath && current.owned) fs.rmSync(current.filePath, { force: true });\n    resetHarnessStore();\n  } else if (current.filePath !== snapshot.filePath) {\n    throw new Error('Harness state pointer changed; refusing to restore an older snapshot.');\n  }\n  for (const [key, value] of Object.entries(snapshot.environment)) {\n    if (value === undefined) delete environment[key];\n    else environment[key] = value;\n  }\n  if (environment === process.env) {\n    harnessCache.loaded = snapshot.filePath\n      ? {\n          filePath: snapshot.filePath,\n          owned: snapshot.owned,\n          state: snapshot.state,\n          mtimeMs: fileMtime(snapshot.filePath),\n        }\n      : undefined;\n  }\n}\n\nexport function getHarnessState(report?: HarnessStateParseReporter): HarnessState {\n  return loadHarnessState(process.env, report).state;\n}\n\nfunction stateFilePath(directory: string): string {\n  return path.join(directory, STATE_FILE_NAME);\n}\n\n/** Where a process writes state when it was not given a file of its own. */\nfunction adoptedStateFilePath(): string {\n  return path.join(os.tmpdir(), ADOPTED_DIRECTORY, `${String(process.pid)}.json`);\n}\n\nfunction persist(filePath: string, state: HarnessState, ownerPid = process.pid): void {\n  writePrivateAtomicJson(filePath, {\n    version: STATE_FILE_VERSION,\n    owner: { pid: ownerPid, startedAt: new Date().toISOString() },\n    state,\n  } satisfies HarnessStateFile);\n}\n\n/**\n * Starts a session: writes the file and publishes the derived environment.\n *\n * The environment is a parameter because the caller is usually describing a\n * process other than itself. The launcher builds the environment for the Pi it\n * is about to spawn, and Doom Team builds the snapshot for a detached child, so\n * neither should touch its own `process.env` to do it.\n */\nexport function createHarnessSession(\n  state: HarnessState,\n  options: {\n    directory: string;\n    environment: NodeJS.ProcessEnv;\n    /**\n     * Leave the file for a process that does not exist yet to claim.\n     *\n     * What a spawner wants: the child owns its state from its first read, in a\n     * directory the spawner manages, so the parent cleaning up its own session\n     * cannot pull the file out from under a detached run.\n     */\n    unclaimed?: boolean;\n  },\n): string {\n  const filePath = stateFilePath(options.directory);\n  persist(filePath, state, options.unclaimed ? UNCLAIMED_OWNER : process.pid);\n  options.environment[HARNESS_STATE_POINTER] = filePath;\n  projectHarnessEnvironment(state, options.environment);\n  // Only prime the cache when this process is the one being described.\n  if (!options.unclaimed && options.environment === process.env)\n    harnessCache.loaded = { filePath, owned: true, state, mtimeMs: fileMtime(filePath) };\n  return filePath;\n}\n\n/**\n * Applies a patch to this process's state.\n *\n * Copy on write: a process holding a file it does not own writes its own copy\n * and repoints itself at it, so a child can never rewrite its parent's session.\n */\nexport function updateHarnessState(\n  patch: Partial<HarnessState>,\n  environment: NodeJS.ProcessEnv = process.env,\n): HarnessState {\n  const current = loadHarnessState(environment);\n  const state = { ...current.state, ...patch };\n  const filePath = current.owned && current.filePath ? current.filePath : adoptedStateFilePath();\n  persist(filePath, state);\n  environment[HARNESS_STATE_POINTER] = filePath;\n  if (environment === process.env) harnessCache.loaded = { filePath, owned: true, state, mtimeMs: fileMtime(filePath) };\n  projectHarnessEnvironment(patch, environment);\n  return state;\n}\n\n/** Removes this process's own state file. Safe when there is none. */\nexport function disposeHarnessState(environment: NodeJS.ProcessEnv = process.env): void {\n  const current = harnessCache.loaded;\n  resetHarnessStore();\n  if (!current?.owned || !current.filePath) return;\n  fs.rmSync(current.filePath, { force: true });\n  delete environment[HARNESS_STATE_POINTER];\n}\n\nexport function harnessRoot(state: Pick<HarnessState, 'root'> = getHarnessState()): string {\n  return state.root ?? process.cwd();\n}\n\n/**\n * The repository root, or a hard error.\n *\n * Separate from `requireHarnessPaths` because most callers only need the root,\n * and separate from `harnessRoot` because falling back to `process.cwd()` would\n * quietly operate on whatever directory the session happens to sit in. A caller\n * that asks for the root wants the session's repository or nothing.\n */\nexport function requireHarnessRoot(state: Pick<HarnessState, 'root'> = getHarnessState()): string {\n  if (!state.root) throw new Error(`${HARNESS_STATE_KEYS.root} is not set`);\n  return state.root;\n}\n\n/** The root and the run's scratch directory, for callers that write to disk. */\nexport function requireHarnessPaths(state: Pick<HarnessState, 'root' | 'temporaryDirectory'> = getHarnessState()): {\n  root: string;\n  temporaryDirectory: string;\n} {\n  const root = requireHarnessRoot(state);\n  if (!state.temporaryDirectory) throw new Error(`${HARNESS_STATE_KEYS.temporaryDirectory} is not set`);\n  return { root, temporaryDirectory: state.temporaryDirectory };\n}\n"],"mappings":"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"}