/**
 * Dispatch Daemon Service
 *
 * This daemon runs continuous polling loops to coordinate task assignment
 * and message delivery across all agents in the orchestration system.
 *
 * Key features:
 * - Worker availability polling: Assigns unassigned tasks to available workers
 * - Inbox polling: Delivers messages and spawns agents when needed
 * - Steward trigger polling: Activates stewards based on scheduled triggers
 * - Workflow task polling: Assigns workflow tasks to available stewards
 *
 * The daemon implements the dispatch behavior defined in ORCHESTRATION_PLAN.md:
 * - Workers are spawned INSIDE their worktree directory
 * - Handoff branches are reused when present in task metadata
 * - Inbox polling: Routes messages by role (triage for ephemeral, forward for persistent)
 *
 * @module
 */
import type { EntityId, ElementId } from '@stoneforge/core';
import type { QuarryAPI, InboxService } from '@stoneforge/quarry';
import type { AgentRegistry } from './agent-registry.js';
import type { SessionManager, SessionRecord } from '../runtime/session-manager.js';
import type { DispatchService } from './dispatch-service.js';
import type { WorktreeManager } from '../git/worktree-manager.js';
import type { TaskAssignmentService } from './task-assignment-service.js';
import type { StewardScheduler } from './steward-scheduler.js';
import type { AgentPoolService } from './agent-pool-service.js';
import type { SettingsService } from './settings-service.js';
/**
 * Default poll interval in milliseconds for dispatch daemon (5 seconds)
 */
export declare const DISPATCH_DAEMON_DEFAULT_POLL_INTERVAL_MS = 5000;
/**
 * Minimum poll interval in milliseconds for dispatch daemon (1 second)
 */
export declare const DISPATCH_DAEMON_MIN_POLL_INTERVAL_MS = 1000;
/**
 * Maximum poll interval in milliseconds for dispatch daemon (1 minute)
 */
export declare const DISPATCH_DAEMON_MAX_POLL_INTERVAL_MS = 60000;
/**
 * Callback fired when a session is started by the dispatch daemon.
 * This allows the server to attach event listeners and save the initial prompt.
 */
export type OnSessionStartedCallback = (session: SessionRecord, events: import('events').EventEmitter, agentId: EntityId, initialPrompt: string) => void;
/**
 * Configuration for the Dispatch Daemon
 */
export interface DispatchDaemonConfig {
    /**
     * Poll interval in milliseconds.
     * Default: 5000 (5 seconds)
     */
    readonly pollIntervalMs?: number;
    /**
     * Whether worker availability polling is enabled.
     * Default: true
     */
    readonly workerAvailabilityPollEnabled?: boolean;
    /**
     * Whether inbox polling is enabled.
     * Default: true
     */
    readonly inboxPollEnabled?: boolean;
    /**
     * Whether steward trigger polling is enabled.
     * Default: true
     */
    readonly stewardTriggerPollEnabled?: boolean;
    /**
     * Whether workflow task polling is enabled.
     * Default: true
     */
    readonly workflowTaskPollEnabled?: boolean;
    /**
     * Whether orphan recovery polling is enabled.
     * Detects workers with assigned tasks but no active session after a restart
     * and re-spawns sessions to continue the work.
     * Default: true
     */
    readonly orphanRecoveryEnabled?: boolean;
    /**
     * Whether plan auto-completion polling is enabled.
     * Detects active plans where all tasks are closed and marks them as completed.
     * Default: true
     */
    readonly planAutoCompleteEnabled?: boolean;
    /**
     * Whether closed-but-unmerged task reconciliation is enabled.
     * Detects tasks with status=CLOSED but mergeStatus not 'merged'
     * and moves them back to REVIEW so merge stewards can pick them up.
     * Default: true
     */
    readonly closedUnmergedReconciliationEnabled?: boolean;
    /**
     * Grace period in ms before a closed-but-unmerged task is reconciled.
     * Prevents racing with in-progress close+merge sequences.
     * Default: 120000 (2 minutes)
     */
    readonly closedUnmergedGracePeriodMs?: number;
    /**
     * Whether stuck-merge recovery is enabled.
     * Detects tasks stuck in 'merging' or 'testing' mergeStatus for too long
     * and resets them to 'pending' for a fresh retry.
     * Default: true
     */
    readonly stuckMergeRecoveryEnabled?: boolean;
    /**
     * Grace period in ms before a stuck merge task is recovered.
     * Prevents racing with in-progress merge operations.
     * Default: 600000 (10 minutes)
     */
    readonly stuckMergeRecoveryGracePeriodMs?: number;
    /**
     * Maximum number of consecutive resume attempts for the same task
     * before the daemon stops resuming the worker and spawns a recovery
     * steward instead. Set to 0 to disable recovery steward spawning.
     * Default: 3
     */
    readonly maxResumeAttemptsBeforeRecovery?: number;
    /**
     * Maximum session duration in ms before the daemon terminates it.
     * Prevents stuck workers from blocking their slot indefinitely.
     * Default: 0 (disabled).
     */
    readonly maxSessionDurationMs?: number;
    /**
     * Maximum session duration in ms for steward sessions specifically.
     * Steward sessions are expected to be short-lived; this provides
     * a safety net for sessions that fail to self-terminate.
     * Default: 1800000 (30 minutes).
     */
    readonly maxStewardSessionDurationMs?: number;
    /**
     * Callback fired when a session is started by the daemon.
     * Allows the server to attach event savers and save the initial prompt.
     */
    readonly onSessionStarted?: OnSessionStartedCallback;
    /**
     * Project root directory for loading prompt overrides.
     * Default: process.cwd()
     */
    readonly projectRoot?: string;
    /**
     * Whether to auto-forward inbox messages to the director's interactive session.
     * When enabled, messages are injected into the director's PTY as user input
     * (same mechanism as persistent workers via processPersistentAgentMessage).
     * Default: true
     */
    readonly directorInboxForwardingEnabled?: boolean;
    /**
     * Minimum idle time (ms) since last user input before forwarding messages
     * to the director's session. Prevents interrupting the user mid-thought.
     * Default: 120000 (2 minutes)
     */
    readonly directorInboxIdleThresholdMs?: number;
}
/**
 * Result of a poll operation
 */
export interface PollResult {
    /** The poll type */
    readonly pollType: 'worker-availability' | 'inbox' | 'steward-trigger' | 'workflow-task' | 'orphan-recovery' | 'closed-unmerged-reconciliation' | 'stuck-merge-recovery' | 'plan-auto-complete';
    /** Timestamp when the poll started */
    readonly startedAt: string;
    /** Duration of the poll in milliseconds */
    readonly durationMs: number;
    /** Number of items processed */
    readonly processed: number;
    /** Number of errors encountered */
    readonly errors: number;
    /** Error messages if any */
    readonly errorMessages?: string[];
}
/**
 * Dispatch Daemon interface for coordinating task assignment and message delivery.
 *
 * The daemon provides methods for:
 * - Starting and stopping the polling loops
 * - Manual trigger of individual poll operations
 * - Configuration management
 */
export interface DispatchDaemon {
    /**
     * Starts the dispatch daemon with all enabled polling loops.
     * Reconciles stale sessions on startup before beginning polls.
     */
    start(): Promise<void>;
    /**
     * Stops the dispatch daemon and all polling loops.
     * Waits for any in-flight poll cycle to complete before returning.
     */
    stop(): Promise<void>;
    /**
     * Whether the daemon is currently running.
     */
    isRunning(): boolean;
    /**
     * Manually triggers worker availability polling.
     * Finds available ephemeral workers and assigns unassigned tasks.
     */
    pollWorkerAvailability(): Promise<PollResult>;
    /**
     * Manually triggers inbox polling.
     * Processes unread messages for all agents.
     */
    pollInboxes(): Promise<PollResult>;
    /**
     * Manually triggers steward trigger polling.
     * Checks for scheduled steward activations.
     */
    pollStewardTriggers(): Promise<PollResult>;
    /**
     * Manually triggers workflow task polling.
     * Assigns workflow tasks to available stewards.
     */
    pollWorkflowTasks(): Promise<PollResult>;
    /**
     * Manually triggers orphan recovery polling.
     * Detects workers with assigned tasks but no active session
     * and re-spawns sessions to continue the work.
     */
    recoverOrphanedAssignments(): Promise<PollResult>;
    /**
     * Manually triggers closed-but-unmerged task reconciliation.
     * Detects tasks with status=CLOSED but mergeStatus not 'merged'
     * and moves them back to REVIEW so merge stewards can pick them up.
     */
    reconcileClosedUnmergedTasks(): Promise<PollResult>;
    /**
     * Manually triggers stuck-merge recovery.
     * Detects tasks stuck in 'merging' or 'testing' mergeStatus for too long
     * and resets them to 'pending' for a fresh retry.
     */
    recoverStuckMergeTasks(): Promise<PollResult>;
    /**
     * Manually triggers plan auto-completion polling.
     * Detects active plans where all non-tombstone tasks are closed
     * and marks them as completed.
     */
    pollPlanAutoComplete(): Promise<PollResult>;
    /**
     * Notify the daemon that a rate limit was detected for an executable.
     * Called externally (e.g., from event listeners on session events).
     */
    handleRateLimitDetected(executable: string, resetsAt: Date): void;
    /**
     * Returns the current rate limit status for the daemon.
     */
    getRateLimitStatus(): {
        isPaused: boolean;
        limits: Array<{
            executable: string;
            resetsAt: string;
        }>;
        soonestReset?: string;
    };
    /**
     * Manually put the daemon to sleep until the specified time.
     * Marks all executables in the fallback chain as rate-limited until the given time.
     * This reuses the existing rate limit tracker and pause logic.
     */
    sleepUntil(resetTime: Date): void;
    /**
     * Immediately wake the daemon from a sleep/rate-limit pause.
     * Clears all rate limit tracker entries and the sleep timer.
     * The next poll cycle will resume normal dispatch.
     */
    wake(): void;
    /**
     * Gets the current configuration.
     */
    getConfig(): Omit<Required<DispatchDaemonConfig>, 'onSessionStarted'> & {
        onSessionStarted?: OnSessionStartedCallback;
    };
    /**
     * Updates the configuration.
     * Takes effect on the next poll cycle.
     */
    updateConfig(config: Partial<DispatchDaemonConfig>): void;
    /**
     * Subscribe to daemon events.
     */
    on(event: 'poll:start', listener: (pollType: string) => void): void;
    on(event: 'poll:complete', listener: (result: PollResult) => void): void;
    on(event: 'poll:error', listener: (pollType: string, error: Error) => void): void;
    on(event: 'task:dispatched', listener: (taskId: ElementId, agentId: EntityId) => void): void;
    on(event: 'message:forwarded', listener: (messageId: string, agentId: EntityId) => void): void;
    on(event: 'agent:spawned', listener: (agentId: EntityId, worktree?: string) => void): void;
    on(event: 'daemon:notification', listener: (data: {
        type: 'info' | 'warning' | 'error';
        title: string;
        message?: string;
    }) => void): void;
    /**
     * Unsubscribe from daemon events.
     */
    off(event: string, listener: (...args: unknown[]) => void): void;
}
/**
 * Implementation of the Dispatch Daemon.
 */
export declare class DispatchDaemonImpl implements DispatchDaemon {
    private readonly api;
    private readonly agentRegistry;
    private readonly sessionManager;
    private readonly dispatchService;
    private readonly worktreeManager;
    private readonly taskAssignment;
    private readonly stewardScheduler;
    private readonly inboxService;
    private readonly poolService;
    private readonly settingsService;
    private readonly rateLimitTracker;
    private readonly emitter;
    private config;
    private running;
    private polling;
    private pollIntervalHandle?;
    private currentPollCycle?;
    private rateLimitSleepTimer?;
    /**
     * Tracks inbox item IDs that are currently being forwarded to persistent agents.
     * Prevents duplicate message delivery when concurrent pollInboxes() calls
     * race to forward the same unread message before markAsRead() completes.
     *
     * Key: inbox item ID
     * Value: true (item is in-flight, being processed)
     *
     * Items are added before forwarding and removed after markAsRead() completes.
     */
    private readonly forwardingInboxItems;
    /**
     * When true, the startup background orphan recovery is still in flight.
     * runPollCycle skips its own orphan recovery to avoid duplicate work and
     * to prevent the initial poll cycle from blocking on stale session resumes.
     */
    private startupRecoveryInFlight;
    constructor(api: QuarryAPI, agentRegistry: AgentRegistry, sessionManager: SessionManager, dispatchService: DispatchService, worktreeManager: WorktreeManager, taskAssignment: TaskAssignmentService, stewardScheduler: StewardScheduler, inboxService: InboxService, config?: DispatchDaemonConfig, poolService?: AgentPoolService, settingsService?: SettingsService);
    start(): Promise<void>;
    stop(): Promise<void>;
    isRunning(): boolean;
    handleRateLimitDetected(executable: string, resetsAt: Date): void;
    getRateLimitStatus(): {
        isPaused: boolean;
        limits: Array<{
            executable: string;
            resetsAt: string;
        }>;
        soonestReset?: string;
    };
    sleepUntil(resetTime: Date): void;
    wake(): void;
    pollWorkerAvailability(): Promise<PollResult>;
    pollInboxes(): Promise<PollResult>;
    pollStewardTriggers(): Promise<PollResult>;
    pollWorkflowTasks(): Promise<PollResult>;
    recoverOrphanedAssignments(): Promise<PollResult>;
    reconcileClosedUnmergedTasks(): Promise<PollResult>;
    /**
     * Detects tasks stuck in 'merging' or 'testing' mergeStatus for too long
     * and resets them to 'pending' for a fresh retry.
     */
    recoverStuckMergeTasks(): Promise<PollResult>;
    /**
     * Detects active plans where all non-tombstone tasks are closed
     * and marks them as completed.
     */
    pollPlanAutoComplete(): Promise<PollResult>;
    getConfig(): Omit<Required<DispatchDaemonConfig>, 'onSessionStarted'> & {
        onSessionStarted?: OnSessionStartedCallback;
    };
    updateConfig(config: Partial<DispatchDaemonConfig>): void;
    on(event: 'poll:start', listener: (pollType: string) => void): void;
    on(event: 'poll:complete', listener: (result: PollResult) => void): void;
    on(event: 'poll:error', listener: (pollType: string, error: Error) => void): void;
    on(event: 'task:dispatched', listener: (taskId: ElementId, agentId: EntityId) => void): void;
    on(event: 'message:forwarded', listener: (messageId: string, agentId: EntityId) => void): void;
    on(event: 'agent:spawned', listener: (agentId: EntityId, worktree?: string) => void): void;
    on(event: 'agent:triage-spawned', listener: (agentId: EntityId, channelId: string, worktree: string) => void): void;
    on(event: 'daemon:notification', listener: (data: {
        type: 'info' | 'warning' | 'error';
        title: string;
        message?: string;
    }) => void): void;
    off(event: string, listener: (...args: unknown[]) => void): void;
    private createPollInterval;
    private normalizeConfig;
    /**
     * Runs a complete poll cycle for all enabled polling loops.
     */
    private runPollCycle;
    /**
     * Resolves the executable path for a session, checking rate limits and applying
     * fallback selection when the primary executable is rate-limited.
     *
     * @param agent - The agent to resolve the executable for
     * @returns The executable path override if fallback was needed, undefined if primary is OK,
     *          or 'all_limited' if all executables in the fallback chain are rate-limited.
     */
    private resolveExecutableWithFallback;
    /**
     * Terminates sessions that have exceeded the configured max duration.
     * Prevents stuck workers from blocking their slot indefinitely.
     *
     * Applies two thresholds:
     * - `maxSessionDurationMs` — general limit for all sessions (0 = disabled)
     * - `maxStewardSessionDurationMs` — stricter limit for steward sessions
     *   (default 30 minutes), which are expected to be short-lived
     */
    private reapStaleSessions;
    /**
     * Recovers a single orphaned task by re-spawning a session for the worker.
     * Tries to resume the previous provider session first (preserves context),
     * falls back to a fresh spawn if no sessionId or resume fails.
     */
    private recoverOrphanedTask;
    /**
     * Recovers a single orphaned merge steward task by resuming or re-spawning.
     * Tries to resume the previous provider session first (preserves context),
     * falls back to a fresh spawn via spawnMergeStewardForTask.
     */
    private recoverOrphanedStewardTask;
    /**
     * Assigns the highest priority unassigned task to a worker.
     * Handles handoff branches by reusing existing worktrees.
     * Respects agent pool capacity limits.
     */
    private assignTaskToWorker;
    /**
     * Creates a worktree for a task assignment.
     * Includes dependency installation so workers have node_modules available.
     */
    private createWorktreeForTask;
    /**
     * Builds the initial prompt for a task assignment.
     * Includes the worker role prompt followed by task-specific details.
     * Fetches the description Document content so handoff notes (appended to
     * description) are automatically included.
     */
    private buildTaskPrompt;
    /**
     * Builds the initial prompt for a merge steward session.
     * Includes the steward role prompt (steward-merge.md) followed by task context.
     *
     * @param task - The task being reviewed
     * @param stewardId - The steward's entity ID
     * @param stewardFocus - The steward's focus area (merge, docs, or custom)
     * @param syncResult - Optional result from pre-spawn branch sync
     */
    private buildStewardPrompt;
    /**
     * Spawns a merge steward session for a task in REVIEW status.
     * Syncs the branch with master before spawning to ensure clean diffs.
     * Respects agent pool capacity limits.
     */
    private spawnMergeStewardForTask;
    /**
     * Spawns a recovery steward for a task stuck in a resume loop.
     *
     * When a worker session exits without calling `sf task complete` or
     * `sf task handoff`, the orphan recovery loop resumes it. After
     * maxResumeAttemptsBeforeRecovery consecutive resumes without a status
     * change, this method is called instead to:
     * 1. Unassign the worker from the task
     * 2. Find an available recovery steward (or any available steward)
     * 3. Spawn a recovery steward session with full task context
     *
     * @param worker - The worker that was assigned to the stuck task
     * @param task - The stuck task
     * @param taskMeta - The task's orchestrator metadata
     */
    private spawnRecoveryStewardForTask;
    /**
     * Builds the initial prompt for a recovery steward session.
     * Includes the steward-recovery role prompt followed by task context,
     * session history, and resume count information.
     *
     * @param task - The stuck task
     * @param stewardId - The recovery steward's entity ID
     * @param taskMeta - The task's orchestrator metadata
     */
    private buildRecoveryStewardPrompt;
    /**
     * Processes an inbox item for an agent.
     * Handles dispatch messages and delegates to role-specific processors.
     */
    private processInboxItem;
    /**
     * Two-path model for ephemeral worker messages:
     * 1. Dispatch message → mark as read (task dispatch handled by pollWorkerAvailability)
     * 2. Non-dispatch message → leave unread if active session (don't forward/interrupt),
     *    or accumulate as deferred item for triage if idle
     *
     * Returns { processed: boolean, deferredItem?: ... } so pollInboxes can batch triage.
     */
    private processEphemeralWorkerMessage;
    /**
     * Process message for persistent workers and directors.
     * - If in session -> forward as user input
     * - Otherwise -> leave for next session
     */
    private processPersistentAgentMessage;
    /**
     * Processes deferred inbox items for idle agents by spawning triage sessions.
     *
     * Groups items by agentId then channelId. For each agent:
     * - Skips if agent now has an active session (messages stay unread for next cycle)
     * - Spawns triage session for the first channel group only (single-session constraint)
     * - Marks those items as read
     *
     * @returns Number of items processed
     */
    private processTriageBatch;
    /**
     * Spawns a triage session for an agent to process deferred messages.
     *
     * Creates a read-only worktree on the default branch, builds the triage prompt
     * with hydrated message contents, starts a headless session, and registers
     * worktree cleanup on session exit.
     */
    private spawnTriageSession;
    /**
     * Builds the triage prompt by loading the message-triage template and
     * hydrating it with the actual message contents.
     */
    private buildTriagePrompt;
    /**
     * Formats a message for forwarding to an agent session.
     * Fetches document content from contentRef to provide actual message text.
     */
    private formatForwardedMessage;
    /**
     * Syncs a task's branch with the main branch before steward review.
     *
     * This ensures that when a merge steward reviews a PR, the diff against
     * master only shows the task's actual changes (not other merged work).
     *
     * @param task - The task to sync
     * @returns SyncResult with success/conflicts/error status
     */
    private syncTaskBranch;
}
/**
 * Creates a DispatchDaemon instance
 */
export declare function createDispatchDaemon(api: QuarryAPI, agentRegistry: AgentRegistry, sessionManager: SessionManager, dispatchService: DispatchService, worktreeManager: WorktreeManager, taskAssignment: TaskAssignmentService, stewardScheduler: StewardScheduler, inboxService: InboxService, config?: DispatchDaemonConfig, poolService?: AgentPoolService, settingsService?: SettingsService): DispatchDaemon;
//# sourceMappingURL=dispatch-daemon.d.ts.map