/** * Task 179 — the invisible background worker. * * Karl asked whether every thread should have "some kind of system co-thread for * doing this kind of work, but that it wouldn't necessarily be visible in the * threads UI", and answered his own poll with "yes — build it as a general * facility, Refresh is just the first user." * * A co-thread is the wrong engine for it, on four grounded counts: its writes * land in its own git worktree on its own branch (task 143 P4), it is drawn in * the sidebar under its master, it shares the master's stored content in BOTH * directions — so a worker that reads a whole file pours it into the master's * memory — and only a thread already mid-turn can create one, which a button * click is not. * * So this is not a chat thread at all. It is a gateway-owned one-shot model call * with no conversation, no history and no sidebar row — `runOneShotModel` plus * the two things that make it a *facility* rather than a bare call: * * 1. **One job per `{thread, kind}` at a time.** A second request while one is * in flight is REFUSED, not queued. Two Refreshes racing on one `Tasks.md` * is the exact failure the whole task exists to prevent, and a queue would * merely delay it. * 2. **A status push** (`running | done | failed`) to whoever is watching, so a * button can spin without the UI inventing a second notion of "busy". * * It deliberately never touches `threadBusy`. That flag drives the sidebar dots * (task 146), the stall check (task 164) and the deferred-message drain (task * 144); a worker that set it would make the sidebar claim a turn was running, * suppress a real stall check, and drain a user's queued message into nothing. */ import { type OneShotModelContext } from './one-shot-model.js'; /** * What a worker run is for. A closed union, not a free string: the key is half of * the in-flight identity, so a typo would silently buy a second concurrent slot * on the same thread — the one thing this module exists to deny. */ export type WorkerKind = 'tasks-refresh'; export type WorkerState = 'running' | 'done' | 'failed'; export interface WorkerStatus { threadName: string; kind: WorkerKind; state: WorkerState; /** Present on `failed`, and on `done` when there is something worth saying. */ detail?: string; } /** Thrown by {@link runWorker} when this `{thread, kind}` is already working. */ export declare class WorkerBusyError extends Error { constructor(kind: WorkerKind); } /** Thrown when the model produced no answer at all (spawn failure, timeout, empty). */ export declare class WorkerNoAnswerError extends Error { constructor(); } /** * Thrown when no model is configured for background workers (task 184). * * Separate from {@link WorkerNoAnswerError} because it is not a failure at all — * it is an unset setting, and the difference is the whole point: "returned no * answer" sends the operator looking for a broken provider. This message is what * the `/tasks` drawer shows verbatim on a failed Refresh, so it names the setting * to fill in rather than the machinery that did not run. */ export declare class WorkerNoModelError extends Error { constructor(); } export declare function isWorkerRunning(threadName: string, kind: WorkerKind): boolean; /** * Where worker progress goes. Registered by the webchat adapter at startup for * the same reason `setThreadActivityListener` and `setUserQueueDelivery` are: * only the adapter can reach the sockets, and this module must not know they * exist. */ type WorkerStatusListener = (status: WorkerStatus) => void; export declare function setWorkerStatusListener(fn: WorkerStatusListener | undefined): void; export interface RunWorkerOptions { /** Configured worker model id (`workerModel`), resolved against both catalogs. */ model?: string; context?: OneShotModelContext; timeoutMs?: number; maxTokens?: number; } /** * Run one prompt as this thread's `kind` worker and return the model's raw text. * * Throws {@link WorkerNoModelError} if no model is configured, * {@link WorkerBusyError} if that slot is taken, and {@link WorkerNoAnswerError} * if the model produced nothing — all before any caller-visible side effect, so a * refused run leaves no trace. * * The unconfigured check comes FIRST, before the slot and before the `running` * push (task 184): there is nothing to be busy with, and a button that spins and * then reports a failure reads as a broken worker rather than an empty setting. * * The slot is released in a `finally`, so a caller that throws while INTERPRETING * the answer (which is where task 179's guard rails live) still frees the button. */ export declare function runWorker(threadName: string, kind: WorkerKind, prompt: string, opts?: RunWorkerOptions): Promise; /** Announce the end of a worker run. Separate from {@link runWorker} because the * caller — not this module — knows whether interpreting the answer succeeded. */ export declare function reportWorkerResult(threadName: string, kind: WorkerKind, state: 'done' | 'failed', detail?: string): void; export {}; //# sourceMappingURL=worker.d.ts.map