/**
 * src/subagents/runner.ts
 *
 * Fire-and-forget subagent runner. The drug-tier feature: when the owner
 * asks "audit my last 30 days of sales and tell me what's wrong", the main
 * agent forks a 10-minute background task with its own brain context, drops
 * an immediate "🔍 على الموضوع — راح أرجع لك خلال ~10 دقايق" reply, and
 * delivers the deliverable when the subagent finishes.
 *
 * Pattern adapted verbatim from OpenClaw's `subagents/runs.json` model:
 *   - expectsCompletionMessage: true (subagent always pings parent on done)
 *   - cleanup: keep (audit trail preserved)
 *   - runTimeoutSeconds: 600-900 (forced kill if it hangs)
 *
 * The subagent prompt explicitly contains:
 *   "[Subagent Context] Results auto-announce to your requester;
 *    do not busy-poll for status."
 *
 * Implementation contract:
 *   - Background tasks run in the same process (no new node fork) — saves
 *     the cost of spinning a new SQLite client. Concurrency-controlled.
 *   - Each subagent creates its own customer_id (`subagent-<runId>`) so
 *     conversation isolation is automatic.
 *   - On completion, the subagent calls back into the channel that launched
 *     it (web chat, WhatsApp, etc.) via the journal.
 */
import { randomUUID } from 'node:crypto';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { processMessage } from '../agent/brain.js';
import { record as journalRecord } from '../autonomy/journal.js';
import { subagentEvents } from './events.js';
const log = createChildLogger('subagents:runner');
// Concurrency cap. Set high enough to allow real swarms (parallel research
// across 8 competitors, etc.) but low enough that a runaway loop doesn't
// exhaust model rate limits. Override via MUASSIS_SUBAGENT_MAX_CONCURRENT.
const MAX_CONCURRENT = parseInt(process.env.MUASSIS_SUBAGENT_MAX_CONCURRENT ?? '8', 10);
let activeCount = 0;
const SUBAGENT_PREAMBLE = `
[Subagent Context]
You are running as a subagent (depth 1/1). Your task was delegated by the main
agent on behalf of the business owner. Results auto-announce to your requester
when you finish; do not busy-poll for status, do not ask intermediate questions
unless absolutely necessary.

You have full access to: web_search, web_fetch, call_api, recall_fact, the
knowledge graph, and read-only file/data tools. AVOID side effects (no
send_email, no zatca_invoice, no exec_command). Your job is to RESEARCH and
RETURN A REPORT — not to act.

Output format:
1. Lead with a 1-line headline ("Found X — recommend Y").
2. Then a structured body (numbered findings, bullets, table if relevant).
3. Then a "Recommended next steps" section the owner can act on.
4. Cite skills used at the start: "🧰 استخدمت مهارات: <names>".

You have ${' '}<TIMEOUT_HINT>${' '} for this task. Pace yourself. If you can't
complete in time, return what you have with a note about what's incomplete.
`.trim();
/**
 * Spawn a subagent. Returns immediately with the run id. Caller polls
 * /api/subagents/:id or listens to the journal for the announce.
 */
export async function spawnSubagent(spec) {
    if (activeCount >= MAX_CONCURRENT) {
        throw new Error(`Subagent quota reached (${MAX_CONCURRENT}). Wait for one to finish.`);
    }
    const id = randomUUID();
    const now = new Date().toISOString();
    const timeoutSec = Math.max(60, Math.min(spec.timeoutSeconds ?? 600, 1800));
    const row = {
        id,
        tenant_id: spec.tenantId,
        parent_session_id: spec.parentSessionId ?? null,
        parent_customer_id: spec.parentCustomerId ?? null,
        task: spec.task,
        status: 'queued',
        result_excerpt: null,
        error: null,
        metadata: spec.metadata ?? {},
        started_at: null,
        finished_at: null,
        duration_ms: null,
        timeout_seconds: timeoutSec,
        cost_usd: 0,
        created_at: now,
    };
    await supabase.from('subagent_runs').insert(row);
    // Fire-and-forget execution
    void executeSubagent(id, spec, timeoutSec).catch(err => {
        log.error({ id, err: err.message }, 'subagent execution failed unexpectedly');
    });
    return row;
}
async function executeSubagent(id, spec, timeoutSec) {
    activeCount++;
    const startTime = Date.now();
    const subagentCustomerId = `subagent-${id}`;
    await supabase
        .from('subagent_runs')
        .update({ status: 'running', started_at: new Date(startTime).toISOString() })
        .eq('id', id);
    // Live progress: emit a start event so any open dashboard subscriber
    // (or the chat stream that spawned this subagent) sees it light up.
    subagentEvents.emit(id, 'start', `Subagent started — "${spec.task.slice(0, 80)}"`, {
        task: spec.task,
        timeout_seconds: timeoutSec,
        parent_customer_id: spec.parentCustomerId ?? null,
    });
    const preamble = SUBAGENT_PREAMBLE.replace('<TIMEOUT_HINT>', `${timeoutSec} seconds`);
    const prompt = `${preamble}\n\n## Your task\n${spec.task}`;
    // Race the brain against the timeout. AbortController on the brain isn't
    // wired today (model providers don't all respect signal yet), so we use
    // Promise.race with a manual timeout — the LLM may keep running but its
    // output is discarded.
    const timeout = new Promise(resolve => setTimeout(() => resolve({ ok: false, reason: 'timeout' }), timeoutSec * 1000));
    // Collect every file the subagent creates so we can auto-deliver them to the
    // originating channel when it finishes — this is what makes "always send what
    // it creates" work for SUBAGENT output (e.g. the leads xlsx), not just files
    // made in the live reply turn. Deduped by path.
    const artifacts = [];
    const seenArtifactPaths = new Set();
    const work = (async () => {
        const r = await processMessage(spec.tenantId, subagentCustomerId, prompt, { recentMessages: [] }, {
            budgetAware: false,
            enableTools: true,
            onArtifact: (a) => {
                if (a?.path && !seenArtifactPaths.has(a.path)) {
                    seenArtifactPaths.add(a.path);
                    artifacts.push(a);
                }
            },
            // Subagents are autonomous and only judged by the quality of their
            // final report — give them enough loops to actually do real work.
            // 8 was too low: a "research 10 competitors" task needs >20 tool
            // calls (search × refine × verify × write). 30 is the new ceiling;
            // the timeout (default 600s) is the real safety, not the loop cap.
            maxToolLoops: 30,
            // Pipe every brain event into the subagent event bus so the
            // dashboard can render LIVE progress (thinking → tool_call →
            // tool_done → response). This is the "see what subagents do"
            // surface — without it the panel would just say "running…".
            onEvent: (ev) => {
                try {
                    if (ev.type === 'thinking') {
                        subagentEvents.emit(id, 'thinking', `يفكّر${ev.intent ? ` · ${ev.intent}` : ''}`);
                    }
                    else if (ev.type === 'tool_call_start') {
                        subagentEvents.emit(id, 'tool_call', `يستخدم ${ev.name}`, { tool: ev.name });
                    }
                    else if (ev.type === 'tool_call_end') {
                        subagentEvents.emit(id, 'tool_done', `انتهى ${ev.name} ${ev.success ? '✓' : '✗'}`, {
                            tool: ev.name, success: ev.success, output_preview: (ev.output ?? '').slice(0, 200),
                        });
                    }
                    else if (ev.type === 'response') {
                        subagentEvents.emit(id, 'log', 'أعدّ المسودة');
                    }
                }
                catch { /* keep going */ }
            },
        });
        return { ok: true, response: r.response, cost: r.cost ?? 0, model: r.model_used };
    })();
    let result;
    try {
        result = await Promise.race([work, timeout]);
    }
    catch (err) {
        activeCount--;
        const dur = Date.now() - startTime;
        await supabase
            .from('subagent_runs')
            .update({
            status: 'failed',
            finished_at: new Date().toISOString(),
            duration_ms: dur,
            error: err.message.slice(0, 500),
        })
            .eq('id', id);
        subagentEvents.emit(id, 'error', `فشل: ${err.message.slice(0, 120)}`);
        await announceFinished(spec, id, 'failed', null, err.message);
        return;
    }
    activeCount--;
    const finishedAt = new Date();
    const durationMs = finishedAt.getTime() - startTime;
    if (result.ok) {
        const excerpt = result.response.slice(0, 800);
        await supabase
            .from('subagent_runs')
            .update({
            status: 'completed',
            finished_at: finishedAt.toISOString(),
            duration_ms: durationMs,
            result_excerpt: excerpt,
            cost_usd: result.cost,
        })
            .eq('id', id);
        subagentEvents.emit(id, 'finished', `اكتمل خلال ${Math.round(durationMs / 1000)} ث`, {
            duration_ms: durationMs, cost_usd: result.cost, result_excerpt: excerpt,
        });
        await announceFinished(spec, id, 'completed', result.response, null, artifacts);
    }
    else {
        await supabase
            .from('subagent_runs')
            .update({
            status: 'timeout',
            finished_at: finishedAt.toISOString(),
            duration_ms: durationMs,
            error: 'timeout',
        })
            .eq('id', id);
        subagentEvents.emit(id, 'error', `انتهى الوقت (${timeoutSec}s)`, { reason: 'timeout' });
        await announceFinished(spec, id, 'timeout', null, `Hit the ${timeoutSec}s cap before finishing.`);
    }
}
/**
 * Auto-announce: write a journal entry with the final result so it shows up
 * on the owner's dashboard without polling. The web chat / WhatsApp / Telegram
 * layer can render journal kind=subagent_result entries as a chat bubble in
 * the original conversation.
 */
async function announceFinished(spec, id, status, response, error, artifacts = []) {
    const headline = status === 'completed'
        ? `✅ subagent done: ${spec.task.slice(0, 60)}`
        : status === 'timeout'
            ? `⏱️ subagent timed out: ${spec.task.slice(0, 60)}`
            : `❌ subagent failed: ${spec.task.slice(0, 60)}`;
    await journalRecord({
        tenantId: spec.tenantId,
        kind: 'subagent_result',
        action: spec.task.slice(0, 200),
        target: id,
        outcome: status === 'completed' ? 'success' : 'failure',
        reason: response ? response.slice(0, 500) : (error ?? undefined),
        metadata: {
            subagent_id: id,
            parent_customer_id: spec.parentCustomerId,
            response_full: response,
        },
    });
    log.info({ id, status }, headline);
    // ALWAYS bubble back through main Muassis. The owner asked for this
    // explicitly: when a subagent finishes, the main agent must announce
    // "I finished X" in the original chat thread. We insert a synthetic
    // outbound message into the messages table tagged to the parent
    // customer (or web-chat default). The dashboard's chat-stream polling
    // + journal-entry surfacing both pick this up.
    try {
        const parentCustomerId = spec.parentCustomerId
            ?? await findDefaultWebCustomer(spec.tenantId);
        if (!parentCustomerId) {
            log.warn({ subagentId: id }, 'subagent finished but no parent customer to announce to');
            return;
        }
        const taskLabel = spec.task.length > 80 ? spec.task.slice(0, 80) + '…' : spec.task;
        let body;
        if (status === 'completed' && response) {
            body =
                `🤖 **خلصت المهمة الفرعية**\n\n` +
                    `**المهمة:** ${taskLabel}\n\n` +
                    `**النتيجة:**\n${response}\n\n` +
                    `_(تم بواسطة وكيل فرعي · معرّف: \`${id.slice(0, 8)}\`)_`;
        }
        else if (status === 'timeout') {
            body =
                `⏱️ **انتهى وقت الوكيل الفرعي قبل أن يخلّص**\n\n` +
                    `**المهمة:** ${taskLabel}\n\n` +
                    `${error || 'تجاوز الوقت المسموح.'}\n\n` +
                    `لو المهمة محتاجة وقت أطول، قُل لي "بحث أعمق" وراح أعطيه ضعف الوقت.`;
        }
        else {
            body =
                `❌ **فشل الوكيل الفرعي**\n\n` +
                    `**المهمة:** ${taskLabel}\n\n` +
                    `${error ?? 'سبب غير محدد.'}\n\n` +
                    `اطلب مني المحاولة مرة ثانية أو غيّر صياغة المهمة.`;
        }
        const { randomUUID } = await import('node:crypto');
        // The messages table has a fixed schema (id, tenant_id, customer_id,
        // session_id, direction, type, content, …). Previously this insert
        // included a `metadata` field that doesn't exist on the table — the
        // SQLite shim silently dropped the entire row, so subagent results
        // never reached the chat (the trust bug the user just reported).
        // Strip metadata and rely on a unique session_id prefix to mark
        // subagent-sourced messages for downstream filtering.
        const insertResult = await supabase.from('messages').insert({
            id: randomUUID(),
            tenant_id: spec.tenantId,
            customer_id: parentCustomerId,
            session_id: `subagent:${id}`,
            direction: 'outbound',
            type: 'text',
            content: body,
            delivery_status: 'sent',
            created_at: new Date().toISOString(),
        });
        // Surface insert errors so future schema drift doesn't silently break
        // this critical path again. The supabase shim returns { error } on
        // failure; the postgres client throws — handle both.
        if (insertResult?.error) {
            log.error({ subagentId: id, parentCustomerId, error: insertResult.error.message }, '❌ subagent message insert FAILED — owner will not see result in chat');
        }
        else {
            log.info({ subagentId: id, parentCustomerId, status }, '📢 subagent result bubbled back to main thread');
        }
        // Also PUSH the result to the requester's originating channel
        // (WhatsApp/Telegram). Without this, a user who delegated the task from
        // WhatsApp/Telegram only sees the result if they open the web dashboard
        // — i.e. kept clueless about work they explicitly asked for. Web/system
        // customers no-op (the dashboard already surfaces them via polling).
        try {
            const { deliverToCustomerChannel } = await import('../delivery/delivery.js');
            // Auto-attach every file the subagent created so the requester gets
            // the actual deliverable (xlsx/pdf/etc) on their channel — not just a
            // text summary. This is the core "always send what it creates" fix.
            await deliverToCustomerChannel(spec.tenantId, parentCustomerId, body, {
                attachments: artifacts,
            });
        }
        catch (err) {
            log.warn({ subagentId: id, error: err.message }, 'subagent result: external-channel push failed (spooled to retry)');
        }
    }
    catch (err) {
        log.warn({ subagentId: id, error: err.message }, 'failed to bubble subagent result back to chat');
    }
}
// Find a default web customer to announce to when parentCustomerId is unset.
// Picks the most-recent web-channel customer (highest last_contact). If none
// exists, the announcement falls back to journal-only.
async function findDefaultWebCustomer(tenantId) {
    const { data } = await supabase
        .from('customers')
        .select('id')
        .eq('tenant_id', tenantId)
        .like('phone', 'web-%')
        .order('last_contact', { ascending: false })
        .limit(1)
        .maybeSingle();
    return data?.id ?? null;
}
export async function listSubagents(tenantId, limit = 20) {
    const { data } = await supabase
        .from('subagent_runs')
        .select('*')
        .eq('tenant_id', tenantId)
        .order('created_at', { ascending: false })
        .limit(limit);
    return (data ?? []);
}
export async function getSubagent(tenantId, id) {
    const { data } = await supabase
        .from('subagent_runs')
        .select('*')
        .eq('tenant_id', tenantId)
        .eq('id', id)
        .maybeSingle();
    return data ?? null;
}
export async function cancelSubagent(tenantId, id) {
    const { data: existing } = await supabase
        .from('subagent_runs')
        .select('status')
        .eq('tenant_id', tenantId)
        .eq('id', id)
        .maybeSingle();
    if (!existing)
        return false;
    const status = existing.status;
    if (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'timeout') {
        return false;
    }
    // We can't currently abort an in-flight processMessage call; mark as
    // cancelled so the announce treats it as such, the actual brain call
    // finishes in the background and is silently discarded.
    await supabase
        .from('subagent_runs')
        .update({
        status: 'cancelled',
        finished_at: new Date().toISOString(),
        error: 'cancelled by user',
    })
        .eq('id', id);
    return true;
}
export function getActiveCount() {
    return activeCount;
}
//# sourceMappingURL=runner.js.map