"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runDoer = runDoer;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const template_schema_js_1 = require("../../lib/template-schema.js");
const cli_health_js_1 = require("../../lib/cli-health.js");
const cli_precheck_js_1 = require("../../lib/cli-precheck.js");
const abortable_sleep_js_1 = require("../../lib/abortable-sleep.js");
const index_js_1 = require("../../lib/db/index.js");
const permissions_js_1 = require("../../lib/settings/permissions.js");
const transport_js_1 = require("../../lib/settings/transport.js");
const concurrency_js_1 = require("../../lib/settings/concurrency.js");
const cli_semaphore_js_1 = require("../cli-semaphore.js");
const index_js_2 = require("../agents/index.js");
const error_detector_js_1 = require("../error-detector.js");
const output_watcher_js_1 = require("../output-watcher.js");
const participantAborts = __importStar(require("../participant-aborts.js"));
const doer_js_1 = require("./doer.js");
const fallback_registry_js_1 = require("./fallback-registry.js");
const prompt_builder_js_1 = require("./prompt-builder.js");
const run_with_fallback_js_1 = require("./run-with-fallback.js");
const sanitize_name_js_1 = require("./sanitize-name.js");
const swap_sidecar_js_1 = require("./swap-sidecar.js");
const template_fallback_js_1 = require("./template-fallback.js");
async function runDoer(chatDir, chatId, phase, phaseIdx, round, work, filesBlock, tmuxMgr, errorDetector, onEvent, abortSignal, repoPath, templateFallbackDoer, priorRoundFeedback) {
    const doerModel = phase.doer.models?.[0];
    const shim = (0, index_js_2.pickShimForVoice)(phase.doer.lineage, doerModel);
    const agentName = shim.name;
    const isHttp = (0, index_js_2.isHttpDispatchedShim)(shim);
    // Pre-spawn precheck: short-circuit doomed runs without paying the spawn
    // tax. Two cheap layers: (1) recent quota_exhausted with future resetAt,
    // (2) credential file missing → user not logged in. HTTP-dispatched shims
    // (openrouter) skip this — their auth is the secrets table, checked
    // inside the shim itself.
    if (!isHttp) {
        const preDoer = await (0, cli_precheck_js_1.precheckLineage)(phase.doer.lineage);
        if (!preDoer.ok) {
            onEvent({
                chatId,
                type: 'cli_warning',
                payload: {
                    phaseId: phase.id,
                    round,
                    role: 'doer',
                    agent: agentName,
                    lineage: phase.doer.lineage,
                    reason: preDoer.reason,
                    message: preDoer.message,
                    cta: preDoer.cta,
                    resetAt: preDoer.resetAt,
                },
                ts: Date.now(),
            });
            return null;
        }
    }
    // Acquire daemon-wide CLI slot (global cap + per-lineage cap). Local
    // CLI only — HTTP-dispatched shims (openrouter) bypass. The slot is
    // held until the doer returns; cross-lineage fallback within the slot
    // doesn't refresh the slot (conservative — see reviewer-driver for
    // the same trade-off). The abortSignal lets a cancelled chat unwind
    // a queued doer without blocking the semaphore head forever.
    // The outer try/finally below guarantees release on every exit path.
    let releaseSlot = null;
    if (!isHttp && concurrency_js_1.CLI_LINEAGES.includes(agentName)) {
        try {
            releaseSlot = await (0, cli_semaphore_js_1.acquire)(agentName, abortSignal);
        }
        catch {
            // Aborted while queued — bail without spawning. Phase loop
            // already treats null doer return as "doer failed".
            return null;
        }
    }
    const roundDir = path_1.default.join(chatDir, `round-${round}`);
    const doerDir = path_1.default.join(roundDir, `doer-${agentName}`);
    if (!fs_1.default.existsSync(doerDir)) {
        fs_1.default.mkdirSync(doerDir, { recursive: true });
    }
    const askFile = path_1.default.join(doerDir, 'ask.md');
    const answerFile = path_1.default.join(doerDir, 'answer.md');
    // Outer try/finally guarantees the cli-semaphore slot is released on
    // every exit path (return null, throw, headless return, tmux return).
    // releaseSlot is null for HTTP shims; the optional-call is the guard.
    try {
        // Resolve doer persona. Falls back to no-persona prompt when the id can't
        // be resolved — emits cli_warning so the cockpit can surface the
        // misconfiguration. Without the warning, retroactive PR #17 review
        // (gemini + opencode-deepseek + opencode-kimi) flagged that a user
        // typoing a persona id silently runs the chat with a generic prompt.
        let doerPersonaPrompt;
        if ('persona' in phase.doer && phase.doer.persona) {
            const personaId = phase.doer.persona;
            try {
                const row = await index_js_1.personas.getById(personaId);
                if (row) {
                    doerPersonaPrompt = row.system_prompt;
                }
                else {
                    onEvent({
                        chatId,
                        type: 'cli_warning',
                        payload: {
                            phaseId: phase.id,
                            phaseIdx,
                            round,
                            role: 'doer',
                            agent: agentName,
                            kind: 'persona_missing',
                            message: `Doer persona "${personaId}" not found in personas table — running with generic prompt. Check the template's doer.persona field.`,
                        },
                        ts: Date.now(),
                    });
                }
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                onEvent({
                    chatId,
                    type: 'cli_warning',
                    payload: {
                        phaseId: phase.id,
                        phaseIdx,
                        round,
                        role: 'doer',
                        agent: agentName,
                        kind: 'persona_lookup_failed',
                        message: `Doer persona lookup for "${personaId}" failed: ${message} — running with generic prompt.`,
                    },
                    ts: Date.now(),
                });
            }
        }
        const ask = (0, prompt_builder_js_1.buildAsk)(phase, phaseIdx, round, work, phase.inputs, filesBlock, doerPersonaPrompt, priorRoundFeedback);
        fs_1.default.writeFileSync(askFile, ask);
        // When the chat was created with a repoPath, the doer's working tree
        // becomes the user's repo (so it can read files + make real edits the
        // ship phase will commit). Reviewers always stay in scratch — they're
        // not allowed to write to the user's repo. ask.md/answer.md still live
        // in the chat dir for artifact viewing.
        const doerCwd = repoPath ?? doerDir;
        // Transport branch: headless when settings + shim support it; else fall
        // through to tmux. Mixed-mode in a single chat is OK — Claude can run
        // headless while a Gemini reviewer falls back to tmux.
        //
        // Per-slot model fallback: phase.doer.models can list multiple models.
        // The chain extends with template.fallback.doer (same lineage, dedup'd).
        // Doer has only one slot, so the dedup just guards against re-trying
        // the slot's own model.
        const transport = await (0, transport_js_1.getTransport)();
        if (transport === 'headless' && shim.runHeadless) {
            const handle = participantAborts.register(chatId, participantAborts.participantKey('doer', agentName), abortSignal);
            try {
                const doerSlot = {
                    lineage: phase.doer.lineage,
                    models: phase.doer.models ?? [],
                };
                const chain = (0, template_fallback_js_1.buildSlotFallbackChain)(doerSlot, [doerSlot], templateFallbackDoer);
                return await (0, run_with_fallback_js_1.runWithChainFallback)(chain, async (entry) => {
                    // Fallback-collision guard, mirroring reviewer-driver. The
                    // doer normally runs alone, but its fallback chain can end at
                    // the same shared template fallback target as the reviewer
                    // slots' chains (e.g. anthropic/claude-sonnet-4-6). Without
                    // this claim, the doer can run claude-sonnet-4-6 then a later
                    // reviewer slot also resolves to claude-sonnet-4-6 and runs
                    // it again — duplicate cost, lineage diversity broken. Sticky
                    // semantics: claim is held through the round on success and
                    // null; throw releases. See fallback-registry.ts for the full
                    // rationale (PR #79 audit, opencode-cli-5 finding).
                    const claimed = (0, fallback_registry_js_1.tryClaim)(chatId, round, entry.lineage, entry.model);
                    if (!claimed) {
                        console.warn(`[doer] fallback collision chat=${chatId} round=${round} ` +
                            `agent=${agentName} ` +
                            `target=${entry.lineage}/${entry.model ?? '(default)'} ` +
                            `— another slot is already running it; advancing chain`);
                        onEvent({
                            chatId,
                            type: 'cli_warning',
                            payload: {
                                phaseId: phase.id,
                                round,
                                role: 'doer',
                                agent: agentName,
                                reason: 'fallback_collision',
                                fromLineage: entry.lineage,
                                toLineage: entry.lineage,
                                fromModel: entry.model ?? '(default)',
                                toModel: entry.model ?? '(default)',
                                message: `Skipping ${entry.lineage}/${entry.model ?? '(default)'} — another slot is already running it. Advancing to next fallback to preserve lineage diversity.`,
                            },
                            ts: Date.now(),
                        });
                        return null;
                    }
                    let threw = false;
                    try {
                        // Cross-lineage swap: when the entry's lineage differs from the
                        // doer's primary, re-resolve the shim. Slot identity (agentName,
                        // doerDir) stays bound to the primary lineage; cli_warning below
                        // surfaces the swap to the cockpit.
                        const entryShim = entry.lineage === phase.doer.lineage
                            ? shim
                            : (0, index_js_2.pickShimForVoice)(entry.lineage, entry.model);
                        // Single-retry on transient kinds before advancing the chain.
                        // See reviewer-driver for full rationale and the kind taxonomy.
                        const MAX_ATTEMPTS = 2;
                        const RETRY_BACKOFF_MS = 1000;
                        for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
                            const lastError = {};
                            const result = await (0, doer_js_1.runDoerHeadless)({
                                shim: entryShim,
                                chatId,
                                phase,
                                round,
                                agentName,
                                askContent: ask,
                                answerFile,
                                doerCwd,
                                abortSignal: handle.signal,
                                onEvent,
                                modelOverride: entry.model,
                                lastError,
                            });
                            if (result !== null)
                                return result;
                            if (attempt === MAX_ATTEMPTS)
                                return null;
                            if (!(0, error_detector_js_1.isRetryableErrorKind)(lastError.kind, entryShim))
                                return null;
                            if (handle.signal.aborted)
                                return null;
                            console.warn(`[doer] retrying transient failure chat=${chatId} round=${round} ` +
                                `agent=${agentName} ` +
                                `target=${entry.lineage}/${entry.model ?? '(default)'} ` +
                                `kind=${lastError.kind} attempt=${attempt + 1}/${MAX_ATTEMPTS}`);
                            onEvent({
                                chatId,
                                type: 'cli_warning',
                                payload: {
                                    phaseId: phase.id,
                                    round,
                                    role: 'doer',
                                    agent: agentName,
                                    reason: 'transient_retry',
                                    message: `Transient ${lastError.kind ?? 'failure'} on ${entry.lineage}/${entry.model ?? '(default)'} — retrying once before advancing fallback.`,
                                },
                                ts: Date.now(),
                            });
                            await (0, abortable_sleep_js_1.abortableSleep)(RETRY_BACKOFF_MS, handle.signal);
                            if (handle.signal.aborted)
                                return null;
                        }
                        return null;
                    }
                    catch (err) {
                        threw = true;
                        throw err;
                    }
                    finally {
                        if (threw) {
                            (0, fallback_registry_js_1.release)(chatId, round, entry.lineage, entry.model);
                        }
                    }
                }, (from, to, fromIdx) => {
                    const sameLineage = from.lineage === to.lineage;
                    const reason = sameLineage ? 'model_fallback' : 'lineage_fallback';
                    const message = sameLineage
                        ? `Doer model "${from.model ?? '(default)'}" produced no answer; retrying with "${to.model ?? '(default)'}".`
                        : `Doer ${from.lineage}/${from.model ?? '(default)'} failed; switching to ${to.lineage}/${to.model ?? '(default)'} (cross-lineage fallback).`;
                    onEvent({
                        chatId,
                        type: 'cli_warning',
                        payload: {
                            phaseId: phase.id,
                            round,
                            role: 'doer',
                            agent: agentName,
                            reason,
                            fromLineage: from.lineage,
                            toLineage: to.lineage,
                            fromModel: from.model ?? '(default)',
                            toModel: to.model ?? '(default)',
                            fallbackIdx: fromIdx,
                            message,
                        },
                        ts: Date.now(),
                    });
                    // Persist to sidecar (see reviewer-driver.ts for rationale).
                    // doerDir is the chat-scoped scratch dir, used here even when
                    // doerCwd was overridden to the user's repo.
                    (0, swap_sidecar_js_1.appendSwapSidecar)(doerDir, {
                        round,
                        phaseId: phase.id,
                        role: 'doer',
                        agent: agentName,
                        reason,
                        fromLineage: from.lineage,
                        toLineage: to.lineage,
                        fromModel: from.model ?? '(default)',
                        toModel: to.model ?? '(default)',
                        fallbackIdx: fromIdx,
                        ts: Date.now(),
                    });
                });
            }
            finally {
                handle.release();
            }
        }
        // Acquire session — fresh per chat by default; reuses across rounds when
        // template policy says so (shareSessionAcrossRounds, default true).
        const perms = await (0, permissions_js_1.getPermissions)();
        const sessionName = (0, sanitize_name_js_1.sanitizeName)(`chorus-${chatId}-${phase.id}-doer-${agentName}`);
        const session = await tmuxMgr.acquire({
            chatId,
            phaseId: phase.id,
            role: 'doer',
            round,
            shareSessionAcrossRounds: phase.iterate.shareSessionAcrossRounds,
            shareSessionAcrossPhases: phase.iterate.shareSessionAcrossPhases,
            shim,
            spawnOpts: {
                sessionName,
                cwd: doerCwd,
                model: phase.doer.models?.[0],
                sandbox: perms.sandboxProfile,
                autoApprove: perms.autoApprovePrompts,
                networkAccess: perms.networkAccess,
            },
            agentName,
        });
        if (shim.clearKeys && shim.clearKeys.length > 0) {
            tmuxMgr.sendKeys(session.name, [...shim.clearKeys]);
        }
        if (shim.preNudge)
            shim.preNudge(session.name);
        const prompt = shim.formatPrompt({
            promptFile: askFile,
            answerFile,
            task: phase.title,
            expectDoneSentinel: true,
        });
        // Wait for the CLI's TUI to finish cold-start before pasting. 6s covers
        // Codex's slow cold-start (it auths + paints panels); shorter and the
        // Enter we send below races against the input box being ready and gets
        // eaten. Raise if a slower box still misses the prompt.
        // abortableSleep so a cancelled chat doesn't wait the full 6s before
        // teardown — PR #77 audit.
        await (0, abortable_sleep_js_1.abortableSleep)(6000, abortSignal);
        if (abortSignal.aborted)
            return null;
        tmuxMgr.pasteBuffer(session.name, prompt);
        // Small gap between paste and Enter so the TUI registers the paste before
        // we submit.
        await (0, abortable_sleep_js_1.abortableSleep)(500, abortSignal);
        if (abortSignal.aborted)
            return null;
        tmuxMgr.sendKeys(session.name, ['Enter']);
        // Poll capture-pane every 2s to surface known CLI failure modes while we
        // wait for the answer file. The detector is stateful for opencode's
        // sustained-error pattern.
        const pollHandle = setInterval(() => {
            try {
                const pane = tmuxMgr.capturePane(session.name);
                const err = errorDetector.inspect(session.name, phase.doer.lineage, pane);
                if (err) {
                    const recoveryKeys = err.kind === 'permission_prompt' ? shim.recoverKeys?.permission_prompt : undefined;
                    if (recoveryKeys && recoveryKeys.length > 0) {
                        // Layer 2 recovery: navigate the dialog, emit a warning (not error),
                        // skip health recording — we recovered, no degradation.
                        tmuxMgr.sendKeys(session.name, [...recoveryKeys]);
                        onEvent({
                            chatId,
                            type: 'cli_warning',
                            payload: {
                                phaseId: phase.id,
                                round,
                                role: 'doer',
                                agent: agentName,
                                recovered: err.kind,
                                keys: [...recoveryKeys],
                                detail: err.detail,
                            },
                            ts: Date.now(),
                        });
                    }
                    else {
                        // Fire-and-forget — recordHealth became async in the libsql
                        // migration. Inside a setInterval callback we can't await without
                        // changing the callback shape; explicit .catch keeps unhandled
                        // rejections off the process and preserves the pre-migration
                        // semantics (non-blocking health record).
                        (0, cli_health_js_1.recordHealth)({
                            lineage: phase.doer.lineage,
                            status: (0, cli_health_js_1.kindToStatus)(err.kind),
                            message: err.message,
                            resetAt: err.resetAt,
                        }).catch((healthErr) => {
                            console.error(`[chorus] recordHealth failed for ${phase.doer.lineage}:`, healthErr);
                        });
                        onEvent({
                            chatId,
                            type: 'cli_error',
                            payload: { phaseId: phase.id, round, role: 'doer', agent: agentName, error: err },
                            ts: Date.now(),
                        });
                    }
                }
            }
            catch {
                // ignore — the watcher will time out independently
            }
        }, 2000);
        try {
            return await (0, output_watcher_js_1.waitForAnswer)(answerFile, {
                timeoutMs: phase.timeoutMs ?? template_schema_js_1.DEFAULT_TMUX_PHASE_TIMEOUT_MS,
                doneSentinel: '## DONE',
            });
        }
        catch {
            return null;
        }
        finally {
            clearInterval(pollHandle);
        }
    }
    finally {
        releaseSlot?.();
    }
}
//# sourceMappingURL=doer-driver.js.map