/** * Query management for Claude SDK * * Handles tracking and lifecycle management of active Claude queries. * Supports concurrent query execution with interrupt capabilities. * * QueryRegistry is a singleton that encapsulates the global activeQueries Map. */ import type { Query } from '@anthropic-ai/claude-agent-sdk'; /** * Registry for tracking active Claude queries. * Singleton — use QueryRegistry.getInstance(). */ export class QueryRegistry { private static instance: QueryRegistry | null = null; private readonly activeQueries = new Map(); private constructor() {} static getInstance(): QueryRegistry { if (!QueryRegistry.instance) { QueryRegistry.instance = new QueryRegistry(); } return QueryRegistry.instance; } /** Reset singleton for testing */ static resetInstance(): void { QueryRegistry.instance = null; } /** Check if there is an active Claude process */ hasActiveProcess(): boolean { return this.activeQueries.size > 0; } /** Check if a specific query is active */ isQueryActive(queryId: string): boolean { return this.activeQueries.has(queryId); } /** Get count of active queries */ getActiveQueryCount(): number { return this.activeQueries.size; } /** Register an active query */ registerQuery(queryId: string, queryInstance: Query): void { this.activeQueries.set(queryId, queryInstance); } /** Unregister an active query */ unregisterQuery(queryId: string): void { this.activeQueries.delete(queryId); } /** * Interrupt a specific Claude query by ID. * @returns true if the query was interrupted, false if not found */ interruptQuery(queryId: string): boolean { const queryInstance = this.activeQueries.get(queryId); if (queryInstance) { queryInstance.interrupt(); this.activeQueries.delete(queryId); return true; } return false; } /** * Interrupt all active Claude queries. * Catches EPIPE errors from the SDK writing to a dying child process. * @returns number of queries that were interrupted */ interruptAllQueries(): number { const count = this.activeQueries.size; for (const [id, queryInstance] of this.activeQueries) { // interrupt() is async and writes to child process stdin. // If the child process has already exited, the write causes EPIPE. // Catch the rejection to prevent unhandled promise rejection. void Promise.resolve(queryInstance.interrupt()).catch(() => { // Expected: EPIPE when child process is already dead }); this.activeQueries.delete(id); } return count; } /** * Interrupt the most recently started Claude query. * @returns true if a query was interrupted, false if no query was running */ interruptCurrentProcess(): boolean { if (this.activeQueries.size === 0) { return false; } this.interruptAllQueries(); return true; } } /** Generate a unique query ID */ export function generateQueryId(): string { return `q-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } export function hasActiveProcess(): boolean { return QueryRegistry.getInstance().hasActiveProcess(); } export function isQueryActive(queryId: string): boolean { return QueryRegistry.getInstance().isQueryActive(queryId); } export function getActiveQueryCount(): number { return QueryRegistry.getInstance().getActiveQueryCount(); } export function registerQuery(queryId: string, queryInstance: Query): void { QueryRegistry.getInstance().registerQuery(queryId, queryInstance); } export function unregisterQuery(queryId: string): void { QueryRegistry.getInstance().unregisterQuery(queryId); } export function interruptQuery(queryId: string): boolean { return QueryRegistry.getInstance().interruptQuery(queryId); } export function interruptAllQueries(): number { return QueryRegistry.getInstance().interruptAllQueries(); } export function interruptCurrentProcess(): boolean { return QueryRegistry.getInstance().interruptCurrentProcess(); }