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; desiredAgentHashes: Map; desiredAgentDefinitions: Map; appliedAgentHashes: Map; bootstrapCandidateAgentHashes: Map | null; hotAddCandidateAgentIds: Set; knownAgentIds: Set; } 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 buildProjectAgentHashes(config: ProjectConfig): Map { return new Map(buildProjectAgentManifest(config).entries.map((entry) => [ path.posix.basename(entry.relativePath, '.md'), entry.contentHash, ])); } function buildProjectAgentDefinitions(config: ProjectConfig): Map { return new Map(config.agents.map((agent) => [agent.id, JSON.stringify(agent)])); } function appliedManifestMatchesDesired(state: ProjectAgentRuntimeState): boolean { return [...state.desiredAgentHashes].every( ([agentId, desiredHash]) => state.appliedAgentHashes.get(agentId) === desiredHash, ); } function refreshAppliedFingerprint(state: ProjectAgentRuntimeState): void { state.appliedFingerprint = appliedManifestMatchesDesired(state) ? state.desiredFingerprint : null; } function createRuntimeState( manager: ProjectAgentRuntimeManager, config: ProjectConfig, options: { forcePending?: boolean } = {}, ): ProjectAgentRuntimeState { const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0; const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown'; const manifest = buildProjectAgentManifest(config); const desiredAgentHashes = buildProjectAgentHashes(config); const desiredAgentDefinitions = buildProjectAgentDefinitions(config); const authoritativeFresh = provenance === 'fresh' && !options.forcePending; return { runtimeGeneration, runtimeGenerationProvenance: provenance, desiredFingerprint: manifest.fingerprint, appliedFingerprint: authoritativeFresh ? manifest.fingerprint : null, bootstrapCandidateFingerprint: provenance === 'starting' && !options.forcePending ? manifest.fingerprint : null, desiredAgentHashes, desiredAgentDefinitions, appliedAgentHashes: authoritativeFresh ? new Map(desiredAgentHashes) : new Map(), bootstrapCandidateAgentHashes: provenance === 'starting' && !options.forcePending ? new Map(desiredAgentHashes) : null, hotAddCandidateAgentIds: new Set(), knownAgentIds: new Set(desiredAgentHashes.keys()), }; } 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 desiredManifest = buildProjectAgentManifest(config); const desiredAgentHashes = buildProjectAgentHashes(config); const desiredAgentDefinitions = buildProjectAgentDefinitions(config); const current = states.get(canonicalPath); if (!current || current.runtimeGeneration !== runtimeGeneration) { const next = createRuntimeState(manager, config); states.set(canonicalPath, next); return next; } for (const [agentId, desiredHash] of desiredAgentHashes) { const previousDesiredHash = current.desiredAgentHashes.get(agentId); if ( current.desiredAgentDefinitions.get(agentId) === desiredAgentDefinitions.get(agentId) && previousDesiredHash && current.appliedAgentHashes.get(agentId) === previousDesiredHash ) { current.appliedAgentHashes.set(agentId, desiredHash); } if ( !current.knownAgentIds.has(agentId) && (provenance === 'fresh' || provenance === 'starting') ) { current.hotAddCandidateAgentIds.add(agentId); } current.knownAgentIds.add(agentId); } for (const agentId of current.hotAddCandidateAgentIds) { if (!desiredAgentHashes.has(agentId)) current.hotAddCandidateAgentIds.delete(agentId); } for (const agentId of current.desiredAgentHashes.keys()) { if (desiredAgentHashes.has(agentId)) continue; current.appliedAgentHashes.delete(agentId); current.bootstrapCandidateAgentHashes?.delete(agentId); } current.desiredFingerprint = desiredManifest.fingerprint; current.desiredAgentHashes = desiredAgentHashes; current.desiredAgentDefinitions = desiredAgentDefinitions; if (provenance !== 'fresh') { current.appliedFingerprint = null; } else if (current.runtimeGenerationProvenance === 'starting') { const bootstrapHashes = current.bootstrapCandidateAgentHashes ?? new Map(); current.appliedAgentHashes = new Map([...bootstrapHashes].filter( ([agentId, hash]) => desiredAgentHashes.get(agentId) === hash, )); refreshAppliedFingerprint(current); } else if (current.runtimeGenerationProvenance !== 'fresh') { current.appliedFingerprint = null; } else { refreshAppliedFingerprint(current); } 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) { const config = await loadValidProjectConfig(canonicalPath); const next = createRuntimeState(manager, config); states.set(canonicalPath, next); return { projectPath: canonicalPath, ...next }; } else if (provenance !== 'fresh') { current.appliedFingerprint = null; current.runtimeGenerationProvenance = provenance; } else if (current.runtimeGenerationProvenance === 'starting') { const bootstrapHashes = current.bootstrapCandidateAgentHashes ?? new Map(); current.appliedAgentHashes = new Map([...bootstrapHashes].filter( ([agentId, hash]) => current.desiredAgentHashes.get(agentId) === hash, )); refreshAppliedFingerprint(current); 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 = createRuntimeState(manager, config, { forcePending: true }); 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 = createRuntimeState(manager, result.config, { forcePending: true }); 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 desiredHash = state.desiredAgentHashes.get(selectedAgentId); if (!desiredHash) { return { ready: false, runtimeGeneration: state.runtimeGeneration }; } const alreadyApplied = state.runtimeGenerationProvenance === 'fresh' && state.appliedAgentHashes.get(selectedAgentId) === desiredHash; const hotAddCandidate = state.runtimeGenerationProvenance === 'fresh' && state.hotAddCandidateAgentIds.has(selectedAgentId); if (!alreadyApplied && !hotAddCandidate) { return { ready: false, runtimeGeneration: state.runtimeGeneration }; } signal?.throwIfAborted(); const liveIds = liveAgentIds(await awaitAbortable(client.listAgents({ signal }), signal)); if (!liveIds.has(selectedAgentId)) { if (alreadyApplied) { state.appliedAgentHashes.delete(selectedAgentId); refreshAppliedFingerprint(state); } return { ready: false, runtimeGeneration: state.runtimeGeneration }; } if (hotAddCandidate) { state.appliedAgentHashes.set(selectedAgentId, desiredHash); state.hotAddCandidateAgentIds.delete(selectedAgentId); refreshAppliedFingerprint(state); } 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 }; }