import { getCurrentBranch, buildCustomCompactionPrompt, fetchAndFormatPlanningState, getImageDescriptions, getRecentToolCallSummaries, storePreCompactionSnapshot, persistCadenceStateToKV, restoreCadenceStateFromKV, formatCompactionDiagnostics, countListItems, } from './compaction-utils';
const COMPLETION_PATTERN = /<promise>\s*DONE\s*<\/promise>/i;
// Ultrawork trigger keywords - case insensitive matching
const ULTRAWORK_TRIGGERS = [
    'ultrawork',
    'ultrathink',
    'ulw',
    'just do it',
    'work hard',
    'plan hard',
    'take a long time',
    'as long as you need',
    'go deep',
    'be thorough',
];
/**
 * Cadence hooks track which sessions are in long-running Cadence mode.
 *
 * These hooks handle:
 * 1. Detect when Cadence mode starts (via command, [CADENCE MODE] tag, or ultrawork triggers)
 * 2. Detect when Cadence completes (via <promise>DONE</promise>)
 * 3. Inject context during compaction (experimental.session.compacting)
 * 4. Trigger continuation after compaction (session.compacted)
 * 5. Clean up on session abort/error
 */
export function createCadenceHooks(ctx, config, dbReader, lastUserMessages) {
    const activeCadenceSessions = new Map();
    const nonCadenceSessions = new Set();
    const NON_CADENCE_CACHE_MAX = 500;
    const log = (msg) => {
        ctx.client.app.log({
            body: {
                service: 'coder-cadence',
                level: 'debug',
                message: msg,
            },
        });
    };
    return {
        async onMessage(input, output) {
            const sessionId = extractSessionId(input);
            if (!sessionId)
                return;
            // Use the USER's message (from chat.params) for trigger detection,
            // not the model's output — avoids false positives when the model
            // uses phrases like "go deep" or "be thorough" in its response.
            // Delete after read — entries are transient (set in chat.params,
            // consumed here in chat.message) so no unbounded Map growth.
            const userText = lastUserMessages?.get(sessionId) ?? '';
            lastUserMessages?.delete(sessionId);
            const cadenceType = getCadenceTriggerType(userText);
            if (cadenceType && !activeCadenceSessions.has(sessionId)) {
                log(`Cadence started for session ${sessionId} via ${cadenceType}`);
                const now = new Date().toISOString();
                const state = {
                    startedAt: now,
                    iteration: 1,
                    maxIterations: 50,
                    lastActivity: now,
                };
                activeCadenceSessions.set(sessionId, state);
                nonCadenceSessions.delete(sessionId);
                persistCadenceStateToKV(sessionId, { ...state }).catch(() => { });
                // If triggered by ultrawork keywords, inject [CADENCE MODE] tag
                if (cadenceType === 'ultrawork') {
                    injectCadenceTag(output);
                }
                showToast(ctx, `⚡ Cadence started · ${state.iteration}/${state.maxIterations}`);
                return;
            }
            // Everything below parses the MODEL's output for structured tags
            // (CADENCE_STATUS, iteration counts, completion signals) that the
            // model intentionally emits — these are NOT false-positive-prone.
            const messageText = extractMessageText(output);
            if (!messageText)
                return;
            // Check if this session is in Cadence mode
            const state = activeCadenceSessions.get(sessionId);
            if (!state) {
                return;
            }
            // Update last activity
            state.lastActivity = new Date().toISOString();
            // Try to extract structured CADENCE_STATUS tag first
            // Format: CADENCE_STATUS loopId={id} iteration={N} maxIterations={max} status={status}
            const statusMatch = messageText.match(/CADENCE_STATUS\s+loopId=(\S+)\s+iteration=(\d+)\s+maxIterations=(\d+)\s+status=(\S+)/i);
            if (statusMatch) {
                const [, loopId, iteration, maxIterations] = statusMatch;
                if (!iteration || !maxIterations)
                    return;
                const newIteration = parseInt(iteration, 10);
                const newMax = parseInt(maxIterations, 10);
                const changed = state.loopId !== loopId ||
                    state.iteration !== newIteration ||
                    state.maxIterations !== newMax;
                state.loopId = loopId;
                state.iteration = newIteration;
                state.maxIterations = newMax;
                if (changed) {
                    const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
                    showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
                    persistCadenceStateToKV(sessionId, { ...state }).catch(() => { });
                }
                return;
            }
            // Fallback: try to extract iteration from loose "iteration: N" pattern
            const iterMatch = messageText.match(/iteration[:\s]+(\d+)/i);
            if (iterMatch && iterMatch[1]) {
                const newIteration = parseInt(iterMatch[1], 10);
                if (newIteration !== state.iteration) {
                    state.iteration = newIteration;
                    const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
                    showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
                    persistCadenceStateToKV(sessionId, { ...state }).catch(() => { });
                }
            }
            // Check for completion signal
            if (COMPLETION_PATTERN.test(messageText)) {
                log(`Cadence completed for session ${sessionId}`);
                activeCadenceSessions.delete(sessionId);
                showToast(ctx, '✅ Cadence loop completed!');
                return;
            }
            // Check for explicit stop/cancel
            if (isCadenceStop(messageText)) {
                log(`Cadence stopped for session ${sessionId}`);
                activeCadenceSessions.delete(sessionId);
                return;
            }
        },
        async onEvent(input) {
            const event = extractEvent(input);
            if (!event)
                return;
            log(`Event received: ${event.type}`);
            // Handle session.compacted - save compaction AND continue loop.
            // Note: Compaction continues in the SAME session (via session.prompt with
            // the existing sessionId), so permissions configured in the config hook
            // (plugin.ts) are automatically inherited — no re-application needed.
            if (event.type === 'session.compacted') {
                const sessionId = event.sessionId;
                if (!sessionId)
                    return;
                const state = activeCadenceSessions.get(sessionId);
                if (!state)
                    return;
                log(`Compaction completed for Cadence session ${sessionId} - saving and continuing`);
                showToast(ctx, '🔄 Compaction saved, resuming Cadence...');
                // Get current git branch
                const branch = await getCurrentBranch();
                try {
                    await ctx.client.session?.prompt?.({
                        path: { id: sessionId },
                        body: {
                            parts: [
                                {
                                    type: 'text',
                                    text: `[CADENCE COMPACTION COMPLETE]

The compaction summary above contains our Cadence session context.

Current branch: ${branch}

1. Have @Agentuity Coder Memory save this compaction:
   - Get existing session: \`agentuity cloud kv get agentuity-opencode-memory "session:${sessionId}" --json --region use\`
   - Append compaction to \`compactions\` array with timestamp
   - Ensure \`cadence\` object exists with: loopId="${state.loopId ?? 'unknown'}", iteration=${state.iteration}, maxIterations=${state.maxIterations}, status="active"
   - Save back to KV and upsert to Vector

After saving the compaction:
1. Read back the session record from KV
2. Return to Lead the PREVIOUS compactions only (not the one just saved - Lead already has the current compaction in context)
3. Format as a readable summary with timestamps
4. Include "what's next" - the Cadence iteration to continue

Response format:
\`\`\`
## Prior Session History: ${sessionId}

### Compaction 1 (timestamp)
[summary]

### Compaction 2 (timestamp)
[summary]

(Current compaction already in your context)

## What's Next
Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
\`\`\`

If no prior compactions exist:
\`\`\`
## Prior Session History: ${sessionId}

No prior compactions - this is the first one.

## What's Next
Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
\`\`\`

2. Then continue the Cadence loop:
   - Review the compaction summary above for context
   - Continue with iteration ${state.iteration}
   - Do NOT restart from the beginning - pick up where we left off`,
                                },
                            ],
                            agent: 'Agentuity Coder Lead',
                        },
                    });
                }
                catch (err) {
                    log(`Failed to save compaction and continue: ${err}`);
                }
            }
            // Handle session.idle - log for debugging/monitoring
            if (event.type === 'session.idle' || event.type === 'session.status') {
                const sessionId = event.sessionId;
                if (!sessionId)
                    return;
                if (activeCadenceSessions.has(sessionId)) {
                    log(`Session ${sessionId} idle while in Cadence mode`);
                }
            }
            // Handle session abort
            if (event.type === 'session.abort' || event.type === 'session.error') {
                const sessionId = event.sessionId;
                if (sessionId && activeCadenceSessions.has(sessionId)) {
                    log(`Cadence aborted for session ${sessionId}`);
                    activeCadenceSessions.delete(sessionId);
                }
            }
        },
        /**
         * Called during context compaction to inject Cadence state.
         * This ensures the compaction summary includes critical loop state.
         * Uses output.prompt to REPLACE the default compaction prompt with
         * enriched context (planning state, images, tool calls, diagnostics).
         */
        async onCompacting(input, output) {
            const sessionId = input.sessionID;
            const state = activeCadenceSessions.get(sessionId);
            if (!state) {
                // Not a Cadence session, nothing to inject
                return;
            }
            log(`Injecting Cadence context during compaction for session ${sessionId}`);
            showToast(ctx, '💾 Compacting Cadence context...');
            // Config flags for compaction behavior
            const compactionCfg = config?.compaction ?? {};
            const useCustomPrompt = compactionCfg.customPrompt !== false;
            const useInlinePlanning = compactionCfg.inlinePlanning !== false;
            const useImageAwareness = compactionCfg.imageAwareness !== false;
            const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
            const maxTokens = compactionCfg.maxContextTokens ?? 4000;
            // 1. Build custom compaction instructions
            const instructions = useCustomPrompt ? buildCustomCompactionPrompt('cadence') : null;
            // 2. Gather enrichment data in parallel
            const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
            const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
                getCurrentBranch(),
                useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
                useImageAwareness
                    ? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
                    : Promise.resolve(null),
                Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
            ]);
            // 3. Build Cadence state section
            const cadenceStateSection = `## CADENCE MODE ACTIVE

This session is running in Cadence mode (long-running autonomous loop).

**Cadence State:**
- Session ID: ${sessionId}
- Loop ID: ${state.loopId ?? 'unknown'}
- Branch: ${branch}
- Started: ${state.startedAt}
- Iteration: ${state.iteration} / ${state.maxIterations}
- Last activity: ${state.lastActivity}

**Session Record Location:**
\`session:${sessionId}\` in agentuity-opencode-memory

After compaction:
1. Memory will save this summary and update the session record
2. Memory should update planning.progress with this compaction
3. Lead will continue the loop from iteration ${state.iteration}
4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?`;
            // 4. Build SQLite dashboard section
            const dashboardSection = buildSqliteDashboardSummary(dbReader, sessionId);
            // 5. Combine everything into the full prompt
            const sections = [];
            if (instructions)
                sections.push(instructions);
            sections.push(cadenceStateSection);
            if (planningState)
                sections.push(planningState);
            if (imageDescs)
                sections.push(imageDescs);
            if (toolSummaries)
                sections.push(toolSummaries);
            if (dashboardSection)
                sections.push(dashboardSection);
            // 6. Add diagnostics
            const stats = {
                planningPhasesCount: countListItems(planningState),
                imageDescriptionsCount: countListItems(imageDescs),
                toolCallSummariesCount: countListItems(toolSummaries),
                estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
            };
            const diagnostics = formatCompactionDiagnostics(stats);
            if (diagnostics)
                sections.push(diagnostics);
            // 7. Enforce token budget
            let fullPrompt = sections.join('\n\n');
            const estimatedTokens = Math.ceil(fullPrompt.length / 4);
            if (maxTokens > 0 && estimatedTokens > maxTokens) {
                // Trim least-critical sections first
                const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(Boolean);
                let trimmed = [...sections];
                for (const candidate of trimOrder) {
                    if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens)
                        break;
                    trimmed = trimmed.filter((s) => s !== candidate);
                }
                fullPrompt = trimmed.join('\n\n');
            }
            // 8. Set the full prompt or push to context
            if (useCustomPrompt) {
                output.prompt = fullPrompt;
            }
            else {
                output.context.push(fullPrompt);
            }
            // 9. Store pre-compaction snapshot to KV (fire-and-forget)
            if (useSnapshotToKV) {
                storePreCompactionSnapshot(sessionId, {
                    timestamp: new Date().toISOString(),
                    sessionId,
                    planningState: planningState ? { raw: planningState } : undefined,
                    cadenceState: state ? { ...state } : undefined,
                    branch,
                }).catch(() => { }); // Fire and forget
            }
        },
        /**
         * Check if a session is currently in Cadence mode.
         * Used by session-memory hooks to avoid double-handling.
         */
        isActiveCadenceSession(sessionId) {
            return activeCadenceSessions.has(sessionId);
        },
        /**
         * Lazy restore: check KV for persisted Cadence state and populate in-memory Map.
         * Called before routing decisions to recover state after plugin restarts.
         * Returns true if state was found and restored.
         */
        async tryRestoreFromKV(sessionId) {
            // Already in memory — nothing to restore
            if (activeCadenceSessions.has(sessionId))
                return true;
            // Known non-Cadence session — skip KV lookup
            if (nonCadenceSessions.has(sessionId))
                return false;
            try {
                const kvState = await restoreCadenceStateFromKV(sessionId);
                if (!kvState) {
                    if (nonCadenceSessions.size >= NON_CADENCE_CACHE_MAX) {
                        nonCadenceSessions.clear();
                    }
                    nonCadenceSessions.add(sessionId);
                    return false;
                }
                const state = {
                    startedAt: kvState.startedAt ?? new Date().toISOString(),
                    loopId: kvState.loopId,
                    iteration: kvState.iteration ?? 1,
                    maxIterations: kvState.maxIterations ?? 50,
                    lastActivity: kvState.lastActivity ?? new Date().toISOString(),
                };
                activeCadenceSessions.set(sessionId, state);
                log(`Restored Cadence state from KV for session ${sessionId} (iteration ${state.iteration}/${state.maxIterations})`);
                return true;
            }
            catch {
                return false;
            }
        },
    };
}
function extractSessionId(input) {
    if (typeof input !== 'object' || input === null)
        return undefined;
    const inp = input;
    if (typeof inp.sessionID === 'string')
        return inp.sessionID;
    if (typeof inp.sessionId === 'string')
        return inp.sessionId;
    return undefined;
}
function extractMessageText(output) {
    if (typeof output !== 'object' || output === null)
        return undefined;
    // Try parts array (Open Code format)
    const out = output;
    if (out.parts && Array.isArray(out.parts)) {
        for (const part of out.parts) {
            if (part.type === 'text' && part.text) {
                return part.text;
            }
        }
    }
    // Try direct text property
    if (typeof out.text === 'string') {
        return out.text;
    }
    return undefined;
}
function extractEvent(input) {
    if (typeof input !== 'object' || input === null)
        return undefined;
    const inp = input;
    if (!inp.event || typeof inp.event.type !== 'string')
        return undefined;
    const sessionId = inp.event.properties?.sessionId ??
        inp.event.properties?.sessionID;
    return { type: inp.event.type, sessionId };
}
function getCadenceTriggerType(text) {
    // Explicit cadence triggers
    if (text.includes('[CADENCE MODE]') || text.includes('agentuity-cadence')) {
        return 'explicit';
    }
    // Check for ultrawork triggers (case insensitive)
    const lowerText = text.toLowerCase();
    if (ULTRAWORK_TRIGGERS.some((trigger) => lowerText.includes(trigger))) {
        return 'ultrawork';
    }
    return null;
}
function injectCadenceTag(output) {
    if (typeof output !== 'object' || output === null)
        return;
    const out = output;
    if (!out.parts || !Array.isArray(out.parts))
        return;
    for (const part of out.parts) {
        if (part.type === 'text' && part.text) {
            part.text = `[CADENCE MODE]\n\n${part.text}`;
            return;
        }
    }
}
function isCadenceStop(text) {
    return (text.includes('status: "cancelled"') ||
        text.includes("status: 'cancelled'") ||
        text.includes('status":"cancelled'));
}
function buildSqliteDashboardSummary(dbReader, parentSessionId) {
    if (!dbReader || !dbReader.isAvailable())
        return undefined;
    const dashboard = dbReader.getSessionDashboard(parentSessionId);
    if (!dashboard.sessions.length)
        return undefined;
    const lines = [];
    for (const node of dashboard.sessions) {
        appendDashboardNode(dbReader, node, 0, lines);
    }
    return `
## SQLite Session Dashboard

- Parent session: ${parentSessionId}
- Total child cost: ${dashboard.totalCost}

${lines.join('\n')}
`;
}
function appendDashboardNode(reader, node, depth, lines) {
    const status = reader.getSessionStatus(node.session.id);
    const todoSummary = node.todoSummary
        ? `${node.todoSummary.pending}/${node.todoSummary.total} pending`
        : 'no todos';
    const costSummary = node.costSummary
        ? `${node.costSummary.totalCost} (${node.costSummary.totalTokens} tokens)`
        : '0 (0 tokens)';
    const label = formatSessionLabel(node.session.title);
    const indent = `${'  '.repeat(depth)}-`;
    lines.push(`${indent} ${label} [${node.session.id}] status: ${status.status}, tools: ${node.activeToolCount}, todos: ${todoSummary}, messages: ${node.messageCount}, cost: ${costSummary}`);
    for (const child of node.children) {
        appendDashboardNode(reader, child, depth + 1, lines);
    }
}
function formatSessionLabel(title) {
    if (!title)
        return 'Session';
    if (title.startsWith('{')) {
        try {
            const parsed = JSON.parse(title);
            if (parsed.description)
                return parsed.description;
            if (parsed.taskId)
                return parsed.taskId;
        }
        catch {
            return title;
        }
    }
    return title;
}
function showToast(ctx, message) {
    try {
        ctx.client.tui.showToast({ body: { message, variant: 'info' } });
    }
    catch {
        // Toast may not be available
    }
}
//# sourceMappingURL=cadence.js.map