/** * FanoutManager — spawns parallel "worker" sessions for a batch of tasks. * * The main agent calls the `session_fanout` tool with a list of independent * tasks (e.g. gap analysis of 20 well-described tasks against the codebase). * Each task becomes a REAL session in the current project: a record is * created in SQLite up front (so it appears in the Studio session list * immediately) and the task prompt is sent through the existing * SessionStreamManager, subject to a concurrency semaphore. * * Fire-and-forget: the manager does not collect results — the user reads * each worker's final message directly in its session. State is process- * local (no new tables); after a restart of `spectral serve` pending worker * turns simply finish like any other session, and the in-memory recursion * guard resets (recursion stays blocked at the tool-registration level * because spawned sessions never outlive the manager's knowledge of them — * see the session-fanout extension). * * Wiring: `configureFanout()` is called once from the SessionStreamManager * constructor (next to `setInterAgentBroker`), which is the single place * where both the manager and the SessionStore are available. */ import type { MetaEvent } from "@aexol/relay-protocol"; import type { SessionStore } from "../storage.js"; import type { SessionStreamManager } from "../session-stream.js"; /** Hard cap on tasks accepted per `start()` call. */ export declare const MAX_TASKS_PER_FANOUT = 20; /** Default number of worker prompts running at the same time. */ export declare const DEFAULT_MAX_CONCURRENT = 4; /** Hard cap on the per-run concurrency requested by the caller. */ export declare const HARD_CAP_CONCURRENT = 8; /** Maximum length of a single task prompt. Longer tasks are skipped. */ export declare const MAX_PROMPT_LENGTH = 12000; /** Maximum length of a task title. */ export declare const MAX_TITLE_LENGTH = 120; /** Maximum length of a task key. */ export declare const MAX_KEY_LENGTH = 80; export interface FanoutTask { key: string; title: string; prompt: string; } export interface FanoutStartInput { parentSessionId: string; projectId: string; tasks: FanoutTask[]; maxConcurrent?: number; } export interface FanoutSessionRef { key: string; sessionId: string; title: string; } export interface FanoutStartResult { runId: string; runLabel: string; /** Concurrency actually applied after clamping. */ maxConcurrent: number; sessions: FanoutSessionRef[]; /** Tasks rejected during validation, with a human-readable reason. */ skipped: Array<{ key: string; reason: string; }>; } /** Minimal surface of SessionStreamManager used by the fanout. */ export interface FanoutManagerDeps { manager: Pick; store: Pick; /** * Optional meta-event publisher — the same channel the REST dispatcher * uses. Publishing `session_created` after each store.createSession is * what makes fanout-spawned sessions appear live in connected UIs; * without it they only show up after a manual refresh. */ publishMetaEvent?: (event: MetaEvent) => void; } /** Error carrying a user-readable reason (surfaced verbatim in the tool result). */ export declare class FanoutError extends Error { } export declare class FanoutManager { private deps; private readonly runs; private readonly activeRunsByProject; /** Every session ever spawned by a fanout in this process (recursion guard). */ private readonly spawnedSessionIds; private runCounter; /** Global concurrency cap across all runs (protects multi-project usage). */ private readonly globalSemaphore; private readonly logger; constructor(logger?: { error: (...args: unknown[]) => void; }); configure(deps: FanoutManagerDeps): void; /** * Wire the meta-event publisher (serve.ts, after the manager exists — * the deferred wrapper no-ops until the relay is up, so ordering is * safe). Without a publisher the tool still works; clients just don't * get live session-list updates. */ setMetaEventPublisher(publish: NonNullable): void; /** Resolve the project id for a working directory, or null. */ resolveProjectId(cwd: string): string | null; /** True if the session was ever spawned as a fanout worker in this process. */ isFanoutChild(sessionId: string): boolean; /** * Validate input, create one session per accepted task (immediately * visible in the session list) and kick off prompt dispatch in the * background. Returns as soon as records exist — never awaits turns. */ start(input: FanoutStartInput): FanoutStartResult; private createWorkerSessions; /** * Publish `session_created` over the relay meta channel so every * subscribed tab refreshes its session list immediately. Mirrors the * dispatcher's safePublish: failures are logged, never propagated — * the session record already exists. */ private publishSessionCreated; private executeRun; } export declare function configureFanout(deps: FanoutManagerDeps): FanoutManager; export declare function getFanoutManager(): FanoutManager | null; /** Test-only: drop the configured singleton. */ export declare function resetFanoutManager(): void; //# sourceMappingURL=fanout-manager.d.ts.map