import { type Task, type TaskGenerationExpectation } from '../core/task-parser.js'; import type { ThreadRecord, RunThreadOptions } from '../core/types/thread-types.js'; import type { PlatformAdapter } from '../platform/index.js'; import type { IncomingMessage } from '../platform/index.js'; /** Test hook: clear in-memory dedup state to simulate a server restart. */ export declare function _testResetCallbackState(): void; /** Child-result notice delivered into a suspended parent's pendingMessages. Echoes the * delegation contract and demands acceptance verification before the result is trusted * (DR-0014 L1 纠偏: verify the deliverable, never the child's self-report). */ export declare function buildChildResultNotice(child: ThreadRecord): string; /** Rebuild run options for a suspended or provider-paused thread. * Restore the persisted status message so resumed updates target the original message. * Lifecycle hooks are resolved by the HookBus from the thread's persisted metadata. */ export declare function buildResumeOptions(parent: ThreadRecord): RunThreadOptions | null; /** Refresh the status message persisted at suspension (metadata.statusMsgRef): a resumed * thread's terminal summary, or the new suspension count if it suspended again. Without * this, the dispatch/webhook status message reads "suspended — waiting on children" * forever after the thread has finished (2026-06-11 verification finding). */ export declare function sealSuspendedStatusMsg(threadId: string, adapter?: PlatformAdapter | null): Promise; export type ResumeFn = (parentThreadId: string) => void; /** Resume a suspended manager thread to answer a subtask's question (ask_manager / DR-0016). * Unlike maybeResumeParent, this does NOT require the manager's child sets to be empty — the * manager is woken to answer, not because its children finished. It answers via answer_subtask, * whose handler sets pendingControl='wait', so the manager re-suspends on its still-live children * at the next step boundary. Shares the `resuming` guard so it never collides with a concurrent * completion-driven resume (whichever wins, the question + any child results both sit in * pendingMessages). No-op unless the manager is currently 'waiting'. */ export declare function resumeManagerForQuestion(managerThreadId: string, resume?: ResumeFn): void; /** Rehydration notice for a freshly rotated manager incarnation: durable artifact first, * tree reconcile second, ledger-pending acceptances third. Mirrors the disaster-join * path — rotation IS a deliberate kill test (DR-0017 D1/D2). */ export declare function buildRehydrationNotice(parent: ThreadRecord, stepsSinceRotation: number): string; /** Rotate an over-threshold manager session before re-entry: retire the persisted session * (clear every slot's sessionId → the next step runs on a FRESH session and gets the full * directive + contract prompt), reset the step base, and queue the rehydration notice. * Only task-artifact (manager) templates rotate; everything uncertain fails open (no * rotation). Returns true iff a rotation happened. */ export declare function maybeRotateManager(threadId: string): Promise; /** Pure: decide whether EVERY remaining awaited task is stuck behind a blocked dependency. * A task is stuck iff some transitive depends_on chain reaches a currently blocked task * (deps are AND-ed: one blocked dependency pins the task forever until someone acts). * Returns null — no deadlock — as soon as any awaited task has a way forward: it is * done/blocked itself (delivery owns it), claimed or pending (in flight), missing * (reconcile owns it), or has no blocked dependency (dispatchable eventually). * The key identifies the stall for the once-per-distinct-stall wake dedup. */ export declare function computeStuckWaitSet(tasks: Task[], waitingIds: string[]): { stuck: string[]; blockers: string[]; key: string; } | null; /** Deadlock notice delivered into the woken manager's pendingMessages: names the stuck * tasks and their blockers, and demands action (mirrors the blocked-escalation notice). */ export declare function buildDeadlockNotice(stuck: string[], blockers: string[]): string; /** Deliver a terminal child's result to its thread parent: remove the child from waitingOn, * queue the result notice into pendingMessages (persistent idempotency via * deliveredChildResults), and resume the parent when nothing is left to wait on. * Orphan children (parent purged or already terminal) degrade to a project-report notice. */ export declare function notifyThreadParent(childId: string, deps?: { resume?: ResumeFn; }): Promise; /** Child-task result notice delivered into a suspended manager's pendingMessages. * completed → acceptance instructions (verify the deliverable, never the report); * blocked → escalation instructions (the child cannot finish on its own). */ export declare function buildTaskResultNotice(task: Task, kind: 'completed' | 'blocked'): string; /** Verify task provenance while holding its mutation lock, then wake waiting managers. */ export declare function notifyTaskParentThreads(taskId: string, kind: 'completed' | 'blocked', deps?: { resume?: ResumeFn; }, ownership?: TaskGenerationExpectation): Promise; type WakeFn = (channel: string, notice: string) => void | Promise; /** Wake (or create) the session on a channel by routing a synthetic user message — the same * mechanism the interactive thread-parent path uses. Shared by thread completion (fireThreadCallback), * task completion (notifyTaskOriginSession), and top-of-tree ask_manager escalation (manager-qa). * agentRunner.route find-or-creates the channel's session, so this works whether or not a live * session still exists. */ /** Message shape wakeSession routes. Exported so the guard side (agent-runner's human-backstop * skip) and tests can stay in sync with the exact synthetic shape by construction. */ export declare function buildSyntheticWakeMessage(channel: string, notice: string, tag: string): IncomingMessage; export declare function wakeSession(channel: string, notice: string, tag: string): Promise; /** Human-facing project-channel notice for a fire-and-forget task queued from inside a thread: * the thread has (most likely) ended and never consumes the result, so nobody is woken — * the outcome just surfaces in the project's report stream. */ export declare function buildThreadOriginTaskNotice(task: Task, kind: 'completed' | 'blocked'): string; type PostNoticeFn = (projectId: string, text: string) => void | Promise; /** Session→task wake (Problem 1): when a task created by an interactive session/agent turns * terminal, route a notice back to its origin channel. Default-on, no fallback — if origin_channel * is set we always wake it. Mutually exclusive with the thread-parent path: if any thread is * currently waiting on this task (waitingOnTasks), that path owns the result and we defer. * Thread-origin tasks (origin_thread_id set — fire-and-forget adds from inside a thread) never * wake a session: the creating thread has likely ended, and its recorded channel is an * unattended dispatch conduit; the result degrades to a durable project-report notice instead. * origin_thread_id takes precedence over origin_channel so legacy tasks that captured both * are fixed retroactively. `wake`/`postNotice` are injectable for testing (mirrors the * `resume` injection on notifyTaskParentThreads). */ export declare function notifyTaskOriginSession(taskId: string, kind: 'completed' | 'blocked', deps?: { wake?: WakeFn; postNotice?: PostNoticeFn; }, ownership?: TaskGenerationExpectation): Promise; /** Sweep one waiting thread's waitingOnTasks against disk state: deliver already-done and * already-blocked children, drop missing ones, keep open ones. Closes the race window * where a child task turns terminal between the suspension snapshot and the waiting * persist (its event fired before anyone was listening). Also the recovery path for task * children — unlike thread children, open tasks survive restarts and stay awaited. */ export declare function reconcileWaitingTasks(threadId: string, deps?: { resume?: ResumeFn; }): Promise; /** After a resumed task-dispatch thread settles TERMINAL, publish the task-tree event that the * dispatch cycle (task-dispatch.ts:281) would have published — but didn't, because the thread * re-entered via a RESUME path (rate-limit resume in resume-dispatcher, OR the DR-0014 * child-completion resume in defaultResume) that bypasses the dispatch cycle entirely. The * worker still marks its task done/blocked on disk, but without this nobody emits the event * that wakes a manager/session waiting on that task — it stays suspended forever (2026-06-29 * finding: rate-limit-resumed leaf task ef14 left manager 5afd → e5be permanently stuck; the * only accidental rescue was a later re-suspension's reconcile-on-suspend sweep). Mirrors the * loose publish at task-dispatch.ts — every subscriber re-verifies disk state (notifyTaskParent * rejects a not-actually-done task; deliveredChildResults dedupes), so a duplicate/stale publish * is a safe no-op. No-op unless the thread is a TERMINAL task-dispatch thread whose task is * done (→ task.completed) or blocked (→ task.blocked) on disk. */ export declare function closeResumedTaskLoop(threadId: string, deps?: { publish?: (e: { type: 'task.completed'; taskId: string; dispatchGeneration?: string | null; } | { type: 'task.blocked'; taskId: string; reason: string; dispatchGeneration?: string | null; }) => void; }): Promise; /** Periodic disk-driven backstop: reconcile EVERY suspended manager's task children against disk * and resume any whose list has emptied. The two fast paths — the task.completed/task.blocked * event (notifyTaskParentThreads) and closeResumedTaskLoop — can each miss a single delivery to a * race: a resume-path settle that reads state a beat early, a loose event rejected before the disk * flip with no re-publish, or a partial TASKS.yaml read mid-commit. Each miss strands a manager on * a child that is ALREADY done/blocked on disk until the next restart (2026-06-29: even with * closeResumedTaskLoop, manager 5afd's completion was never delivered to its parent e5be). This * sweep is purely disk-driven and idempotent (deliveredChildResults dedupes), so it eventually * wakes any such manager regardless of which fast path failed. Returns the number swept. */ export declare function sweepWaitingManagers(deps?: { resume?: ResumeFn; }): Promise; /** Start the periodic waiting-manager sweep. No-op when the startup interval is disabled. */ export declare function startWaitingManagerSweep(): void; /** Register the EventBus subscribers that wake suspended manager threads on child-task * terminal events (DR-0014 §8). Call once at startup, before recoverWaitingThreads. */ export declare function registerTaskTreeSubscribers(bus: { subscribe: (type: any, fn: (e: any) => void) => unknown; }): void; /** Startup recovery: re-deliver results that completed while the server was down. * Idempotent — safe to call repeatedly. Thread children still marked waiting whose records * are gone are treated as failed (the restart already failed all in-flight running threads, * so every surviving child THREAD record is terminal by the time this runs). Task children * are reconciled against disk — open ones stay awaited. Returns the number * of suspended parents processed. */ export declare function recoverWaitingThreads(deps?: { resume?: ResumeFn; }): Promise; /** * Fire the completion callback for an MCP-spawned thread once it is terminal. * - Non-terminal statuses (e.g. a parent that suspended via thread_wait and returned * from runThread in 'waiting') are ignored — suspension is not completion. * - Interactive parent (no parentThreadId): wake the parent by routing a synthetic turn onto * its channel via agentRunner.route. * - Thread-agent parent (parentThreadId set): deliver into the parent thread's * pendingMessages and resume it when its waitingOn empties (notifyThreadParent). * Threads not spawned via thread_start (no parentSessionId) are ignored. */ export declare function fireThreadCallback(threadId: string): Promise; export {};