import { randomUUID } from "node:crypto"; import { DelegationControlError, type DelegationControlApi, type DelegationControlRegistration, type DelegationStartInput, type DelegationWatchApi, type HarnessInspectInput, type ThreadListInput, type ThreadReadInput, type ThreadWaitInput, type ThreadWatchInput, } from "./delegation-types.js"; import { isMergedListCursor, listSortSeconds, mergeThreadListPages, } from "./delegation-thread-list-merge.js"; import { DelegationWatchService } from "./delegation-watch.js"; import type { DelegationWatchStore } from "./delegation-watch-store.js"; function only(values: readonly T[], message: string): T { const value = values.length === 1 ? values[0] : undefined; if (!value) { throw new DelegationControlError("PARENT_THREAD_AMBIGUOUS", message, { matchingRuntimeCount: values.length, }); } return value; } export class DelegationControlRegistry implements DelegationControlApi, DelegationWatchApi { readonly #registrations = new Set(); // Changes whenever the session set changes, so a merged list cursor cannot outlive it. The // instance id keeps a cursor from matching a rebuilt Registry at the same count. readonly #instanceId = randomUUID(); #registrationGeneration = 0; #harnessCatalog: DelegationControlRegistration | undefined; // Watches sit above the sessions so either end may belong to any registered session. readonly #watchService: DelegationWatchService; readonly #remoteRead: ((input: ThreadReadInput) => ReturnType) | undefined; constructor( options: { diagnose?: (error: unknown) => void; remoteRead?: (input: ThreadReadInput) => ReturnType; watchStore?: DelegationWatchStore; } = {}, ) { this.#remoteRead = options.remoteRead; this.#watchService = new DelegationWatchService(this, { ...(options.diagnose ? { diagnose: options.diagnose } : {}), ...(options.watchStore ? { store: options.watchStore } : {}), }); } get size(): number { return this.#registrations.size; } register( registration: DelegationControlRegistration, options: { harnessCatalog?: boolean } = {}, ): () => void { this.#registrations.add(registration); this.#registrationGeneration += 1; if (options.harnessCatalog) this.#harnessCatalog = registration; return () => { if (this.#registrations.delete(registration)) this.#registrationGeneration += 1; if (this.#harnessCatalog === registration) this.#harnessCatalog = undefined; }; } async inspect(input: HarnessInspectInput) { if (this.#harnessCatalog) return this.#harnessCatalog.inspect(input); const registrations = [...this.#registrations]; return only( registrations, "Harness inspection requires exactly one active Host Runtime session", ).inspect(input); } async listHarnesses() { if (this.#harnessCatalog) return this.#harnessCatalog.listHarnesses(); return only( [...this.#registrations], "Harness discovery requires exactly one active Host Runtime session", ).listHarnesses(); } async start(input: DelegationStartInput) { return (await this.#registrationForStart(input)).start(input); } async send(input: Parameters[0]) { return (await this.#registrationForThread(input.threadId)).send(input); } async cancel(input: Parameters[0]) { return (await this.#registrationForThread(input.threadId)).cancel(input); } async read(input: ThreadReadInput) { const { callerThreadId, ...read } = input; if (read.hostId !== undefined && read.hostId !== "local") { if (typeof callerThreadId === "string" && callerThreadId) { const callers = await this.#matching( (registration) => registration.executionHostId?.(callerThreadId) === read.hostId && registration.ownsThread(callerThreadId), ); if (callers.length > 1) only(callers, "Calling Thread has ambiguous execution Host context"); if (callers.length === 1) { const { hostId, ...local } = read; const snapshot = await (await this.#registrationForThread(local.threadId)).read(local); return { ...snapshot, hostId }; } } if (!read.hostId || !this.#remoteRead) throw new DelegationControlError( "RUNTIME_UNREACHABLE", "Remote Thread reading is unavailable", ); return this.#remoteRead(read); } return (await this.#registrationForThread(read.threadId)).read(read); } async wait(input: ThreadWaitInput) { if (input.hostId !== undefined && input.hostId !== "local") throw new DelegationControlError("INVALID_ARGUMENT", "Remote Hosts support thread read only"); return (await this.#registrationForThread(input.threadId)).wait(input); } async watch(input: ThreadWatchInput) { return this.#watchService.watch(input); } async watches() { return this.#watchService.watches(); } /** Stops watching in this process; stored watches resume in the next Runtime. */ close(): void { this.#watchService.close(); } async list(input: ThreadListInput) { if ( (input.parentThreadId || this.#harnessCatalog) && input.cursor && isMergedListCursor(input.cursor) ) { throw new DelegationControlError( "INVALID_ARGUMENT", "List cursor does not match the current Thread list source", ); } if (input.parentThreadId) { return (await this.#registrationForThread(input.parentThreadId)).list(input); } // Shared-backend catalogs see all native and external Threads. Attached // clients are not independent list sources. if (this.#harnessCatalog) return this.#harnessCatalog.list(input); const registrations = [...this.#registrations]; if (registrations.length === 0) { throw new DelegationControlError( "PARENT_THREAD_AMBIGUOUS", "Thread list requires an active Host Runtime session", { matchingRuntimeCount: 0 }, ); } if (registrations.length === 1) { // A merged cursor was issued for several sessions; its positions mean nothing to one. if (input.cursor && isMergedListCursor(input.cursor)) { throw new DelegationControlError( "INVALID_ARGUMENT", "List cursor does not match the active Host Runtime sessions", ); } return only(registrations, "unreachable").list(input); } // Registration order is insertion order, which the merged cursor's session positions follow. return mergeThreadListPages( registrations.map((registration) => (page: ThreadListInput) => registration.list(page)), input, (left, right) => this.#compareThreads(left, right, input.sort), `${this.#instanceId}:${this.#registrationGeneration}`, ); } async #registrationForStart(input: DelegationStartInput): Promise { const matches = await this.#matching((registration) => registration.canHandleStart(input)); return only( matches, input.parentThreadId ? "Parent Thread is not owned by exactly one active Host Runtime session" : "Parent Thread cannot be inferred uniquely; pass --parent-thread explicitly", ); } async #registrationForThread(threadId: string): Promise { const registrations = [...this.#registrations]; const matches = await this.#matching((registration) => registration.ownsThread(threadId)); if (matches.length === 1) return matches[0] as DelegationControlRegistration; if (matches.length > 1) { throw new DelegationControlError( "PARENT_THREAD_AMBIGUOUS", "Thread is not owned by exactly one active Host Runtime session", { matchingRuntimeCount: matches.length }, ); } if (this.#harnessCatalog) return this.#harnessCatalog; if (registrations.length === 0) { throw new DelegationControlError( "PARENT_THREAD_AMBIGUOUS", "Thread is not owned by exactly one active Host Runtime session", { matchingRuntimeCount: 0 }, ); } // When only one runtime session exists, forward unknown thread IDs to it so it can attempt official fallback (or return THREAD_NOT_FOUND). if (registrations.length === 1) return registrations[0] as DelegationControlRegistration; throw new DelegationControlError( "PARENT_THREAD_AMBIGUOUS", "Thread is not owned by exactly one active Host Runtime session", { matchingRuntimeCount: 0 }, ); } #compareThreads( left: Awaited>["threads"][number], right: Awaited>["threads"][number], sort: ThreadListInput["sort"], ): number { const seconds = (thread: typeof left): number | undefined => sort.startsWith("created") ? listSortSeconds(thread, "createdAt") : sort.startsWith("recency") ? (listSortSeconds(thread, "recencyAt") ?? listSortSeconds(thread, "updatedAt")) : listSortSeconds(thread, "updatedAt"); const direction = sort.endsWith("asc") ? 1 : -1; const leftValue = seconds(left) ?? 0; const rightValue = seconds(right) ?? 0; return (leftValue - rightValue) * direction || left.threadId.localeCompare(right.threadId); } async #matching( predicate: (registration: DelegationControlRegistration) => boolean | Promise, ): Promise { const registrations = [...this.#registrations]; const matches = await Promise.all(registrations.map((registration) => predicate(registration))); return registrations.filter((_, index) => matches[index]); } }