import { clearCompletionSidecar } from "./completion-sidecar.ts"; import { createOperationArtifactsAllocator, type OperationArtifacts, type OperationArtifactsAllocator, } from "./operation-artifacts.ts"; import { createCompletionOperationId as allocateCompletionOperationId, createOperationReference, } from "./operation-identity.ts"; import { OPERATION_ENVIRONMENT_KEYS, SUBAGENT_ENV_ACTIVITY_FILE, SUBAGENT_ENV_CANCELLATION_FILE, SUBAGENT_ENV_CANCELLATION_REQUEST_FILE, SUBAGENT_ENV_COMPLETION_FILE, SUBAGENT_ENV_NAME, SUBAGENT_ENV_OPERATION_ID, } from "./operation-env.ts"; import type { NativeLaunchPlan, NativeStartupAdapter, NativeStartupResult, } from "./native-startup.ts"; import { samePortablePath } from "./path-utils.ts"; /** The destination used for one Fresh Completion operation. */ export type CompletionDeliveryMode = "foreground" | "background"; /** Allocate the identity for one independent Fresh Completion operation. */ export function createCompletionOperationId(): string { return allocateCompletionOperationId(); } function withoutOperationEnvironment(env: Record): Record { return Object.fromEntries( Object.entries(env).filter(([key]) => !OPERATION_ENVIRONMENT_KEYS.has(key)), ); } function assertLaunchCwd(expected: string, actual: string): void { if (typeof actual !== "string" || actual.length === 0) { throw new Error("Completion operation launch requires a working directory."); } if (!samePortablePath(expected, actual)) { throw new Error( `Completion operation launch cwd does not match the resolved child cwd: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}.`, ); } } export interface NativeOperationContext { operationId: string; /** Operation artifacts carry all temporary protocol and launch state. */ artifacts: OperationArtifacts; activityFile: string; launchPromptFile: string; startedAt: number; name: string; task: string; deliveryMode: CompletionDeliveryMode; launch: NativeLaunchPlan; surface: string; } export interface CompletionOperationDescription { startedAt: number; name: string; task: string; deliveryMode: CompletionDeliveryMode; } /** Resources made available while a Fresh launch plan is being prepared. */ export interface OperationLaunchScope { operation: "start"; operationId: string; artifacts: OperationArtifacts; launchPromptFile: string; activityFile: string; } export type CompletionOperationAdoption = (context: NativeOperationContext) => T | PromiseLike; export interface FreshCompletionOperationInput extends CompletionOperationDescription { childCwd: string; /** Caller-owned runtime policy; required for Fresh. */ resolveLaunch: (scope: OperationLaunchScope) => NativeLaunchPlan; /** Reserve the Nested Subagent before Native startup begins. */ reserve?: (scope: OperationLaunchScope) => void; /** Release a reservation when startup or adoption fails. */ release?: (scope: OperationLaunchScope) => void; /** Parent-owned RunningSubagent/lifecycle registration seam. */ adopt: CompletionOperationAdoption; } export interface CompletionOperationPreparerDependencies { native: NativeStartupAdapter; /** Operation namespaces are private OS-temporary directories in production. */ operationArtifacts?: OperationArtifactsAllocator; /** Test-only override for the production allocator's temporary root. */ temporaryDirectory?: string; } export interface CompletionOperationPreparer { fresh(input: FreshCompletionOperationInput): Promise; } export type CompletionOperationErrorPhase = "preparation" | "native"; export type CompletionOperationNativeFailure = "rejected" | "unknown"; /** Structured failure from operation preparation or Native startup. */ export class CompletionOperationError extends Error { readonly phase: CompletionOperationErrorPhase; readonly outcome: CompletionOperationNativeFailure; readonly operationId: string; readonly causeError: unknown; constructor(params: { message: string; phase: CompletionOperationErrorPhase; outcome: CompletionOperationNativeFailure; operationId: string; causeError: unknown; }) { super(params.message); this.name = "CompletionOperationError"; this.phase = params.phase; this.outcome = params.outcome; this.operationId = params.operationId; this.causeError = params.causeError; } } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function surfaceFromAdapterError(error: unknown): string | undefined { if (error == null || typeof error !== "object") return undefined; const surface = (error as { surface?: unknown }).surface; return typeof surface === "string" && surface.length > 0 ? surface : undefined; } function normalizeNativeStartupResult(value: unknown): NativeStartupResult { if (value == null || typeof value !== "object" || Array.isArray(value)) { return { kind: "unknown", error: new Error("Native startup adapter returned a malformed result."), }; } const result = value as Record; const kind = result.kind; const surface = typeof result.surface === "string" && result.surface.length > 0 ? result.surface : undefined; if (kind === "accepted" && surface) return { kind, surface }; if (kind === "rejected" || kind === "unknown") { if (!Object.prototype.hasOwnProperty.call(result, "error")) { return { kind: "unknown", ...(surface ? { surface } : {}), error: new Error("Native startup adapter returned a malformed result."), }; } return { kind, ...(surface ? { surface } : {}), error: result.error }; } return { kind: "unknown", ...(surface ? { surface } : {}), error: new Error("Native startup adapter returned a malformed result."), }; } async function cleanupSurface( native: NativeStartupAdapter, surface: string | undefined, ): Promise { if (!surface) return; try { await native.close(surface); } catch { // Surface cleanup is best effort and must not replace the structured // startup outcome. } } function cleanupCompletionEvidence( operationId: string, artifacts: OperationArtifacts | undefined, ): void { if (!artifacts) return; try { clearCompletionSidecar(createOperationReference(operationId, artifacts)); } catch { // Evidence cleanup is best effort and must not hide the startup failure. } } function cleanupOperationArtifacts(artifacts: OperationArtifacts | undefined): void { if (!artifacts) return; try { artifacts.release(); } catch { // Temporary artifact cleanup is best effort and must not hide startup failure. } } function operationLaunch( launch: NativeLaunchPlan, description: CompletionOperationDescription, operationId: string, activityFile: string, artifacts: OperationArtifacts, ): NativeLaunchPlan { const operationEnvironment: Record = { [SUBAGENT_ENV_NAME]: description.name, [SUBAGENT_ENV_OPERATION_ID]: operationId, [SUBAGENT_ENV_ACTIVITY_FILE]: activityFile, [SUBAGENT_ENV_COMPLETION_FILE]: artifacts.path("completion"), [SUBAGENT_ENV_CANCELLATION_REQUEST_FILE]: artifacts.path("cancellationRequest"), [SUBAGENT_ENV_CANCELLATION_FILE]: artifacts.path("cancellation"), }; return { ...launch, env: { ...withoutOperationEnvironment(launch.env), ...operationEnvironment, }, }; } function acceptedContext( description: CompletionOperationDescription, operationId: string, artifacts: OperationArtifacts, activityFile: string, launchPromptFile: string, launch: NativeLaunchPlan, surface: string, ): NativeOperationContext { return { operationId, artifacts, activityFile, launchPromptFile, startedAt: description.startedAt, name: description.name, task: description.task, deliveryMode: description.deliveryMode, launch, surface, }; } function preparationError( description: CompletionOperationDescription, operationId: string, error: unknown, ): CompletionOperationError { return new CompletionOperationError({ message: `Completion operation preparation was rejected for subagent "${description.name}": ${errorMessage(error)}`, phase: "preparation", outcome: "rejected", operationId, causeError: error, }); } function nativeError( description: CompletionOperationDescription, outcome: CompletionOperationNativeFailure, operationId: string, error: unknown, ): CompletionOperationError { return new CompletionOperationError({ message: `Native startup ${outcome} for subagent "${description.name}": ${errorMessage(error)}`, phase: "native", outcome, operationId, causeError: error, }); } interface OperationPreparationState { operationId: string; activityFile?: string; artifacts?: OperationArtifacts; } interface PreparedFreshOperation { artifacts: OperationArtifacts; activityFile: string; launchPromptFile: string; launch: NativeLaunchPlan; reserve?: (scope: OperationLaunchScope) => void; release?: (scope: OperationLaunchScope) => void; adopt: CompletionOperationAdoption; } function createDefaultAllocator( dependencies: CompletionOperationPreparerDependencies, ): OperationArtifactsAllocator { if (dependencies.operationArtifacts) return dependencies.operationArtifacts; return createOperationArtifactsAllocator( dependencies.temporaryDirectory ? { temporaryDirectory: dependencies.temporaryDirectory } : undefined, ); } async function runFresh( dependencies: CompletionOperationPreparerDependencies, allocator: OperationArtifactsAllocator, input: FreshCompletionOperationInput, ): Promise { const state: OperationPreparationState = { operationId: createCompletionOperationId(), }; let reservationAttempted = false; let startupAttempted = false; let prepared: PreparedFreshOperation | undefined; const scope = (): OperationLaunchScope => { if (!prepared) throw new Error("Completion operation launch scope is unavailable."); return { operation: "start", operationId: state.operationId, artifacts: prepared.artifacts, launchPromptFile: prepared.launchPromptFile, activityFile: prepared.activityFile, }; }; const releaseReservation = (): void => { if (!reservationAttempted) return; reservationAttempted = false; try { prepared?.release?.(scope()); } catch { // Reservation cleanup must not hide the original failure. } }; const cleanup = (): void => { cleanupCompletionEvidence(state.operationId, state.artifacts); cleanupOperationArtifacts(state.artifacts); }; try { state.artifacts = allocator.allocate(state.operationId); state.activityFile = state.artifacts.path("activity"); const launchPromptFile = state.artifacts.path("launchPrompt"); prepared = { artifacts: state.artifacts, activityFile: state.activityFile, launchPromptFile, launch: (() => { const launch = input.resolveLaunch({ operation: "start", operationId: state.operationId, artifacts: state.artifacts!, launchPromptFile, activityFile: state.activityFile!, }); assertLaunchCwd(input.childCwd, launch.cwd); return operationLaunch( launch, input, state.operationId, state.activityFile!, state.artifacts!, ); })(), reserve: input.reserve, release: input.release, adopt: input.adopt, }; if (prepared.reserve) { prepared.reserve(scope()); // Only a successfully accepted reservation may be released on a later // startup/adoption failure. A rejected duplicate must not release an // existing operation with the same identity. reservationAttempted = true; } startupAttempted = true; const startup = normalizeNativeStartupResult(await dependencies.native.start({ displayName: input.name, operationId: state.operationId, cwd: prepared.launch.cwd, env: prepared.launch.env, args: prepared.launch.args, task: prepared.launch.task, operation: "start", })); if (startup.kind !== "accepted" || !startup.surface) { await cleanupSurface(dependencies.native, startup.surface); releaseReservation(); cleanup(); throw nativeError( input, startup.kind === "accepted" ? "unknown" : startup.kind, state.operationId, startup.kind === "accepted" ? new Error("Native startup was accepted without a usable surface.") : startup.error, ); } try { const adopted = await prepared.adopt(acceptedContext( input, state.operationId, prepared.artifacts, prepared.activityFile, prepared.launchPromptFile, prepared.launch, startup.surface, )); reservationAttempted = false; return adopted; } catch (error) { await cleanupSurface(dependencies.native, startup.surface); releaseReservation(); cleanup(); throw nativeError(input, "unknown", state.operationId, error); } } catch (error) { if (error instanceof CompletionOperationError) throw error; if (!startupAttempted) { releaseReservation(); cleanup(); throw preparationError(input, state.operationId, error); } await cleanupSurface(dependencies.native, surfaceFromAdapterError(error)); releaseReservation(); cleanup(); throw nativeError(input, "unknown", state.operationId, error); } } export function createCompletionOperationPreparer( dependencies: CompletionOperationPreparerDependencies, ): CompletionOperationPreparer { const allocator = createDefaultAllocator(dependencies); return { fresh: (input) => runFresh(dependencies, allocator, input), }; }