import { realpathSync } from 'node:fs'; import path from 'node:path'; import type { ProjectConfig } from '../../shared/project-config'; import type { OpencodeAgentInfo } from './client'; import { buildProjectAgentManifest, readProjectConfig, } from './project-config'; interface ProjectAgentRuntimeManager { getRuntimeGeneration?: () => number; getRuntimeGenerationProvenance?: () => 'unknown' | 'starting' | 'fresh' | 'attached'; } interface ProjectAgentRegistryClient { listAgents: (options?: { signal?: AbortSignal }) => Promise; } type RuntimeGenerationProvenance = 'unknown' | 'starting' | 'fresh' | 'attached'; interface ProjectAgentRuntimeState { runtimeGeneration: number; runtimeGenerationProvenance: RuntimeGenerationProvenance; desiredFingerprint: string; appliedFingerprint: string | null; bootstrapCandidateFingerprint: string | null; } export interface ProjectAgentRuntimeSnapshot extends ProjectAgentRuntimeState { projectPath: string; } export interface ProjectAgentPreflightResult { ready: boolean; runtimeGeneration: number; } export type ProjectAgentAcceptanceResult = | { ready: false; runtimeGeneration: number } | { ready: true; runtimeGeneration: number; value: T }; export interface ProjectAgentRuntimeMutation { previousConfig: ProjectConfig | null; config: ProjectConfig; value: T; } const runtimeStates = new WeakMap>(); const runtimeLocks = new WeakMap>>(); function canonicalProjectPath(projectPath: string): string { try { return realpathSync.native(projectPath); } catch { return path.resolve(projectPath); } } function getManagerMap(registry: WeakMap>, manager: object): Map { let entries = registry.get(manager); if (!entries) { entries = new Map(); registry.set(manager, entries); } return entries; } async function withProjectRuntimeLock( manager: object, projectPath: string, operation: () => Promise, signal?: AbortSignal, ): Promise { const locks = getManagerMap(runtimeLocks, manager); const previous = locks.get(projectPath) ?? Promise.resolve(); let release!: () => void; const current = new Promise((resolve) => { release = resolve; }); const queued = previous.then(() => current); locks.set(projectPath, queued); try { await awaitAbortable(previous, signal); signal?.throwIfAborted(); return await operation(); } finally { release(); void queued.then(() => { if (locks.get(projectPath) === queued) { locks.delete(projectPath); } }); } } async function loadValidProjectConfig(projectPath: string): Promise { const result = await readProjectConfig(projectPath); if (result.status !== 'valid') { throw new Error('Project configuration is missing or invalid'); } return result.config; } function observeState( manager: ProjectAgentRuntimeManager, canonicalPath: string, config: ProjectConfig, ): ProjectAgentRuntimeState { const states = getManagerMap(runtimeStates, manager as object); const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0; const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown'; const desiredFingerprint = buildProjectAgentManifest(config).fingerprint; const current = states.get(canonicalPath); if (!current || current.runtimeGeneration !== runtimeGeneration) { const next = { runtimeGeneration, runtimeGenerationProvenance: provenance, desiredFingerprint, appliedFingerprint: provenance === 'fresh' ? desiredFingerprint : null, bootstrapCandidateFingerprint: provenance === 'starting' ? desiredFingerprint : null, }; states.set(canonicalPath, next); return next; } current.desiredFingerprint = desiredFingerprint; if (provenance !== 'fresh') { current.appliedFingerprint = null; } else if (current.runtimeGenerationProvenance === 'starting') { current.appliedFingerprint = current.bootstrapCandidateFingerprint === desiredFingerprint ? desiredFingerprint : null; } else if (current.runtimeGenerationProvenance !== 'fresh') { current.appliedFingerprint = null; } current.runtimeGenerationProvenance = provenance; return current; } export async function observeProjectAgentRuntime( manager: ProjectAgentRuntimeManager, projectPath: string, knownConfig?: ProjectConfig, signal?: AbortSignal, ): Promise { const canonicalPath = canonicalProjectPath(projectPath); return await withProjectRuntimeLock(manager as object, canonicalPath, async () => { signal?.throwIfAborted(); const config = knownConfig ?? await loadValidProjectConfig(canonicalPath); signal?.throwIfAborted(); const state = observeState(manager, canonicalPath, config); return { projectPath: canonicalPath, ...state }; }, signal); } export async function observeProjectAgentRuntimeGeneration( manager: ProjectAgentRuntimeManager, projectPath: string, signal?: AbortSignal, ): Promise { const canonicalPath = canonicalProjectPath(projectPath); return await withProjectRuntimeLock(manager as object, canonicalPath, async () => { signal?.throwIfAborted(); const states = getManagerMap(runtimeStates, manager as object); const current = states.get(canonicalPath); if (!current) { const config = await loadValidProjectConfig(canonicalPath); signal?.throwIfAborted(); const state = observeState(manager, canonicalPath, config); return { projectPath: canonicalPath, ...state }; } const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0; const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown'; if (current.runtimeGeneration !== runtimeGeneration) { current.runtimeGeneration = runtimeGeneration; current.runtimeGenerationProvenance = provenance; current.appliedFingerprint = provenance === 'fresh' ? current.desiredFingerprint : null; current.bootstrapCandidateFingerprint = provenance === 'starting' ? current.desiredFingerprint : null; } else if (provenance !== 'fresh') { current.appliedFingerprint = null; current.runtimeGenerationProvenance = provenance; } else if (current.runtimeGenerationProvenance === 'starting') { current.appliedFingerprint = current.bootstrapCandidateFingerprint === current.desiredFingerprint ? current.desiredFingerprint : null; current.runtimeGenerationProvenance = provenance; } else if (current.runtimeGenerationProvenance !== 'fresh') { current.appliedFingerprint = null; current.runtimeGenerationProvenance = provenance; } return { projectPath: canonicalPath, ...current }; }, signal); } export async function markProjectAgentRuntimePending( manager: ProjectAgentRuntimeManager, projectPath: string, config: ProjectConfig, ): Promise { const canonicalPath = canonicalProjectPath(projectPath); return await withProjectRuntimeLock(manager as object, canonicalPath, async () => { const state = { runtimeGeneration: manager.getRuntimeGeneration?.() ?? 0, runtimeGenerationProvenance: manager.getRuntimeGenerationProvenance?.() ?? 'unknown', desiredFingerprint: buildProjectAgentManifest(config).fingerprint, appliedFingerprint: null, bootstrapCandidateFingerprint: null, }; getManagerMap(runtimeStates, manager as object).set(canonicalPath, state); return { projectPath: canonicalPath, ...state }; }); } export async function mutateProjectAgentRuntime( manager: ProjectAgentRuntimeManager, projectPath: string, mutation: () => Promise>, ): Promise { const canonicalPath = canonicalProjectPath(projectPath); return await withProjectRuntimeLock(manager as object, canonicalPath, async () => { const result = await mutation(); if (result.previousConfig) { observeState(manager, canonicalPath, result.previousConfig); observeState(manager, canonicalPath, result.config); } else { const state: ProjectAgentRuntimeState = { runtimeGeneration: manager.getRuntimeGeneration?.() ?? 0, runtimeGenerationProvenance: manager.getRuntimeGenerationProvenance?.() ?? 'unknown', desiredFingerprint: buildProjectAgentManifest(result.config).fingerprint, appliedFingerprint: null, bootstrapCandidateFingerprint: null, }; getManagerMap(runtimeStates, manager as object).set(canonicalPath, state); } return result.value; }); } function liveAgentIds(agents: OpencodeAgentInfo[]): Set { return new Set(agents.flatMap((agent) => { const name = typeof agent.name === 'string' ? agent.name.trim() : ''; const id = typeof agent.id === 'string' ? agent.id.trim() : ''; return [name, id].filter(Boolean); })); } async function awaitAbortable(promise: Promise, signal?: AbortSignal): Promise { if (!signal) return await promise; signal.throwIfAborted(); return await new Promise((resolve, reject) => { const onAbort = () => reject(signal.reason); signal.addEventListener('abort', onAbort, { once: true }); promise.then( (value) => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); }, ); }); } export async function acceptProjectAgentRuntime( manager: ProjectAgentRuntimeManager, projectPath: string, client: ProjectAgentRegistryClient, selectedAgentId: string, accept: () => Promise, signal?: AbortSignal, ): Promise> { const canonicalPath = canonicalProjectPath(projectPath); return await withProjectRuntimeLock(manager as object, canonicalPath, async () => { signal?.throwIfAborted(); const config = await loadValidProjectConfig(canonicalPath); signal?.throwIfAborted(); const state = observeState(manager, canonicalPath, config); const configuredAgentIds = config.agents.map((agent) => agent.id); if ( !configuredAgentIds.includes(selectedAgentId) || state.appliedFingerprint !== state.desiredFingerprint ) { return { ready: false, runtimeGeneration: state.runtimeGeneration }; } signal?.throwIfAborted(); const liveIds = liveAgentIds(await awaitAbortable(client.listAgents({ signal }), signal)); if (configuredAgentIds.some((agentId) => !liveIds.has(agentId))) { state.appliedFingerprint = null; return { ready: false, runtimeGeneration: state.runtimeGeneration }; } signal?.throwIfAborted(); const value = await awaitAbortable(accept(), signal); return { ready: true, runtimeGeneration: state.runtimeGeneration, value, }; }, signal); } export async function preflightProjectAgentRuntime( manager: ProjectAgentRuntimeManager, projectPath: string, client: ProjectAgentRegistryClient, selectedAgentId: string, ): Promise { const result = await acceptProjectAgentRuntime( manager, projectPath, client, selectedAgentId, async () => undefined, ); return { ready: result.ready, runtimeGeneration: result.runtimeGeneration }; }