import { IFileWatcher } from './core/adapters'; import { ProviderId } from './providers'; export type LoopState = 'idle' | 'running' | 'stopping' | 'paused'; export interface LoopCallbacks { /** Send a prompt to the active AI provider. messageFile is the absolute path of the written .md file for CLI providers. */ sendToAi: (prompt: string, taskLabel: string, includeProfile?: boolean, messageFile?: string) => Promise; /** Append a message to the extension's output channel */ log: (msg: string) => void; /** Called whenever the loop state changes so the sidebar can refresh */ onStatusChange: (state: LoopState, currentTask?: string) => void; /** Called when Claude's current tool activity changes (undefined = idle/done) */ onActivityChange?: (activity: string | undefined) => void; /** Called once when the queue drains (all tasks done). Used by `--once` to stop. */ onAllTasksDone?: () => void; /** Returns the currently selected provider ID (live, not from settings file) */ getActiveProvider: () => ProviderId; /** Transiently override the active provider (e.g. fallback on rate limit). */ setActiveProvider?: (id: ProviderId) => void; /** Absolute path to the workspace root directory */ workspaceRoot: string; /** File watcher used to monitor TODO.md and output files */ fileWatcher: IFileWatcher; } export declare class TaskLoopRunner { private _state; private _currentTask; private _taskWatcher; private _iterations; private _cb; private _webhook; private _settings; private _workspaceRoot; private _discordPoller; private _discordGateway; private _webhookPoller; private _emailPoller; /** True after we last told the server "all_tasks_done" — cleared on task_start. * Used to re-assert idle state on WS reconnect, otherwise agent_online flips * the server-side status back to 'active' even though we have no work. */ private _idleNotified; private _pollerIntervals; private _hooksFileOffset; /** Recently-forwarded hook-line hashes → first-seen timestamp (ms). * Used to suppress byte-identical hook events that get appended multiple * times to the shared JSONL (Copilot CLI fires the same hook from every * parallel session in the same workspace, all writing to one homedir * file). Entries older than HOOKS_DEDUPE_WINDOW_MS are pruned each tick. */ private _hookLineSeen; private _taskCompletionAbort; private _retryScheduler; private _resumeResolve; /** Resolves the idle no-task sleep early when a poller appends a new task. */ private _idleSleepWake; private _resumeAt; /** When the loop is paused for reauth/rate-limit, the event+payload to re-emit * on a WS reconnect INSTEAD of agent_online/task_start. Reconnects are routine * (heartbeat, NAT blips, server restart); without this a reconnect during a * pause flips the server status back to active/working and erases the * operator's 'needs reauth' / 'rate limited' badge, stranding the agent. * Cleared on retry()/resume. */ private _pauseReason; /** When fallback is active: the saved main provider and when to switch back. */ private _mainProviderBeforeFallback; private _mainProviderResumeAt; private _gitRepo; private _gitBranch; private _hostname; private _completedCount; private _failedCount; private _loopStartTime; /** Task lines that have already had /compact run — prevents infinite compact loops. */ private _compactedTaskLines; /** True while a compact operation is in progress — prevents nested/recursive compacts. */ private _compacting; /** Timestamp (ms) when compact was last run — used to throttle auto-compact (minimum 2min between compacts). */ private _lastCompactTime; /** Dispatch attempt counter per task key (id or text). After 3 failed attempts the * loop force-marks the task done so it doesn't block the queue indefinitely. */ private _taskAttempts; /** Task keys (id or text) flagged as provider hard-failures this session. * These must NOT be auto-completed by the give_up / stranded-[~] heuristics — * a blocked task stays [~] + reported failed until it is retried. Cleared when * a fresh [ ] dispatch of the same task begins (provider presumably recovered). */ private _blockedTasks; /** Counts completed tasks since the last auto-compact run. */ private _autoCompactCounter; /** Counts completed tasks since the last session reset. */ private _resetSessionCounter; /** Counts tasks dispatched since the last profile-included send. */ private _profileSentCounter; /** Manages all "every N tasks" periodic action counters. */ private readonly _periodicMgr; /** Parsed .autodev/CONTROL.md, or null when absent (=> today's behavior). */ private _controlSpec; /** taskKey → git HEAD sha captured before the task's FIRST dispatch (revert target). */ private _preTaskCommit; /** taskKey → mechanical auto-fix passes already spent on this task. */ private _verifyMech; /** A verify failure hint to fold into the NEXT dispatch of the matching task. */ private _pendingFixHint; get state(): LoopState; get currentTask(): string | undefined; get resumeAt(): Date | undefined; private _git; /** Current HEAD sha, or '' if not a git repo / no commits. */ private _gitHead; /** Hard-reset the working tree back to a previous commit (discard the task's work). */ private _gitResetHard; /** Stage everything and commit; a no-op commit (nothing changed) is swallowed. */ private _gitCommitAll; /** Files changed since `sha` (tracked diff + untracked), repo-relative. */ private _gitChangedFiles; /** Compact "+A/-D" line-delta since `sha` for the journal ΔC column. */ private _gitDeltaComplexity; /** Manually trigger a /compact on the current session for the given provider/root. */ compact(root: string, provider: ProviderId): Promise; /** Manually trigger a /clear on the current Claude session for the given provider/root. */ clearSession(root: string, provider: ProviderId): Promise; /** Resume the loop after a rate-limit pause. Clears the scheduled timer. */ retry(): void; start(callbacks: LoopCallbacks): Promise; stop(): void; /** * Stop the loop and, once it reaches idle, start it again with the same * callbacks. Useful for picking up new MCP server configs etc. */ restart(): Promise; /** Handle mcp_update pushed from pixel-office: write .mcp.json, sync all providers, restart loop. */ private _handleMcpUpdate; /** * Handle skill_update pushed from pixel-office: validate + write the agent's * FULL effective skill set to `.claude/skills//SKILL.md`. Skills * live-reload (Claude re-reads them each run), so this does NOT restart the * loop — it just sanitizes, full-replaces on disk, folds a prose block into * AGENTS.md for non-Claude providers, and reports the applied names. */ private _handleSkillUpdate; /** Handle export_request from pixel-office: create backup zip and upload. */ private _handleExportRequest; /** Handle restore_request from pixel-office: download zip and restore workspace. */ private _handleRestoreRequest; /** Handle export_config from pixel-office: persist exportEnabled + exportDailyBackup to settings. */ private _handleExportConfig; /** Check if a daily backup is due and trigger it automatically. */ private _checkDailyBackup; private _saveLastBackupTime; /** Dispatch a slash command received from any inbound channel. */ private _handleCommand; /** * Handle an instant/steer message pushed over the WS. Unlike a normal task * (which is queued to TODO and picked up on the next poll), a steer is meant * to reach the agent *now*: * * • claude-tui, mid-turn → inject the text straight into the running turn * (true mid-turn steering via the live stdin session). * • otherwise (idle, or a provider without a live session) → append to TODO * and wake the idle sleep so it runs at the very next turn boundary rather * than waiting for a poll interval. This is the documented fallback. */ private _handleSteer; /** Flush steers buffered during a run into TODO at a turn boundary (no active * provider run), so a provider's own TODO rewrite can't clobber them. */ private _pendingSteers; private _drainPendingSteers; /** * Build an EmailTaskPoller from the Email MCP entry's env block, or return * null if the feature is disabled or required IMAP creds are missing. */ private _buildEmailPoller; /** * Start Discord and webhook server pollers as independent setInterval loops. * They run continuously in the background — even while the AI is processing a task. */ private _startPollers; /** * Run `cozempic init` in the given project directory if: * 1. `cozempic` is on the PATH (or login-shell PATH on Unix) * 2. The project hasn't been initialised yet * (`.claude/settings.local.json` does not contain a cozempic hook entry) * * Runs synchronously in a background thread-pool task (spawnSync) so it * doesn't block the VS Code event loop but still logs completion. */ private _runCozempicInit; private _stopPollers; private _runLoop; /** * Suspend the loop in 'paused' state. * Resolves when retry() is called or (optionally) the timer fires. * MUST be called only from _runLoop. */ private _pauseLoop; /** Interrupt the idle no-task sleep — called by pollers when they append a task. */ private _wakeIdleSleep; /** sleep() that resolves early when _wakeIdleSleep() is called. */ private _sleepOrWake; /** sleep() that resolves immediately when the task-completion abort fires. */ private _sleepAbortable; /** Return when the task text appears with [x] status in the TODO.md file. */ private _waitForTaskCompletion; private _disposeWatcher; private _setState; private _notifyWebhook; private _notifyDiscord; } /** Singleton runner — one loop per workspace session. */ export declare const taskLoopRunner: TaskLoopRunner;