import { SHORTCUT_LLM_PROXY_URL } from '../../../constants.js';
import { DURABLE_MEMORY_AGENT_MODEL, DURABLE_MEMORY_AGENT_TOOLS, DURABLE_MEMORY_TURN_END_CADENCE } from '../../extensions/durable-memory/constants.js';
import { buildDurableMemoryPromptFragment } from '../../extensions/durable-memory/retrieval.js';
import { emitDurableMemoryStatus } from '../../extensions/durable-memory/status.js';
import { validateWorkspaceState } from '../../extensions/durable-memory/validator.js';
import { DurableMemoryWorkspace } from '../../extensions/durable-memory/workspace.js';
import { deriveModePermissionPolicy } from '../../permissions/policy.js';
import { createShortcutAuthRecovery } from '../../providers/shortcut-auth-recovery.js';
import { resolveForkTraceDir } from '../../session/fork-trace-paths.js';
import { runForkedAgent } from '../../session/forked-agent.js';
import { durableMemoryAgent } from './memory-agent.js';
import { createDurableMemoryTools } from './memory-tools.js';
const MEMORY_AGENT_INSTRUCTION = 'Review the conversation and update durable memory topic files only if a durable change is warranted. End with one very concise sentence, maximum one sentence, summarizing whether durable memory changed.';
const DURABLE_MEMORY_FORK_KIND = 'durable-memory';
const DURABLE_MEMORY_FORK_MAX_TURNS = 5;
const DURABLE_MEMORY_TOOL_TEXT_MAX_CHARS = 1_500;
const DURABLE_MEMORY_TOOL_ARGS_MAX_CHARS = 500;
/** Collapse whitespace and return undefined when the agent's response is empty. */
function summarizeAgentResponse(response) {
    const normalized = response.replace(/\s+/g, ' ').trim();
    return normalized.length > 0 ? normalized : undefined;
}
/** Emit a durable-memory running-status update tagged with the active tool call. */
function emitRunningToolCall(toolCall) {
    emitDurableMemoryStatus({
        phase: 'running',
        toolCall: { name: toolCall.name, arguments: toolCall.arguments }
    });
}
/** Build a checkpoint record describing what triggered this durable-memory run. */
function buildCheckpoint(type, sessionId, traceRef) {
    return {
        type,
        promptMode: type === 'turn_end' ? 'working' : 'consolidation',
        sessionId,
        timestamp: new Date().toISOString(),
        traceRefs: [traceRef]
    };
}
/** Build a stable trace reference combining session id and the current completed-turn count. */
function getTurnTraceRef(ctx) {
    const completedTurns = getCompletedHumanTurnCount(ctx);
    return `session:${ctx.sessionManager.getSessionId()}#turn:${completedTurns}`;
}
/** Trim and ellipsize `text` to at most `maxChars` characters. */
function truncateText(text, maxChars) {
    const normalized = text.trim();
    if (normalized.length <= maxChars)
        return normalized;
    return `${normalized.slice(0, maxChars - 1)}…`;
}
/** Pass tool arguments through unless their JSON exceeds the cap, in which case return a preview record. */
function truncateToolArguments(argumentsValue) {
    if (!argumentsValue || typeof argumentsValue !== 'object' || Array.isArray(argumentsValue)) {
        return {};
    }
    const json = JSON.stringify(argumentsValue);
    if (!json || json.length <= DURABLE_MEMORY_TOOL_ARGS_MAX_CHARS) {
        return argumentsValue;
    }
    return {
        truncated: true,
        preview: truncateText(json, DURABLE_MEMORY_TOOL_ARGS_MAX_CHARS)
    };
}
/** Filter and truncate session messages into the trimmed view fed to the durable-memory fork. */
function toConversationMessages(messages) {
    return messages.flatMap((message) => {
        if (!message || typeof message !== 'object' || !('role' in message)) {
            return [];
        }
        const candidate = message;
        if (candidate.role === 'toolResult') {
            const text = candidate.content
                .filter((item) => item.type === 'text')
                .map((item) => item.text)
                .join('\n')
                .trim();
            if (text.length === 0) {
                return [];
            }
            return [
                {
                    ...candidate,
                    content: [
                        {
                            type: 'text',
                            text: truncateText(text, DURABLE_MEMORY_TOOL_TEXT_MAX_CHARS)
                        }
                    ]
                }
            ];
        }
        if (!('content' in candidate) || !Array.isArray(candidate.content)) {
            return [candidate];
        }
        if (candidate.role === 'user') {
            const content = candidate.content.filter((item) => !!item && typeof item === 'object' && item.type === 'text');
            if (content.length === 0) {
                return [];
            }
            return [{ ...candidate, content }];
        }
        const content = candidate.content.filter((item) => !!item && typeof item === 'object' && (item.type === 'text' || item.type === 'toolCall'));
        if (content.length === 0) {
            return [];
        }
        return [
            {
                ...candidate,
                content: content.map((item) => item.type === 'toolCall'
                    ? { ...item, arguments: truncateToolArguments(item.arguments) }
                    : item)
            }
        ];
    });
}
/** Type guard for session-branch entries that expose a string `type` field. */
function isSessionEntryWithType(entry) {
    return (!!entry &&
        typeof entry === 'object' &&
        'type' in entry &&
        typeof entry.type === 'string');
}
/** True if a session entry is a user-role message. */
function isUserMessageEntry(entry) {
    if (!isSessionEntryWithType(entry) || entry.type !== 'message')
        return false;
    const candidate = entry;
    return candidate.message?.role === 'user';
}
/** Walk the session branch and count completed turns that contained at least one user message. */
function getHumanTurnState(ctx) {
    const branch = ctx.sessionManager.getBranch?.() ?? [];
    let currentTurnHasUserMessage = false;
    let completedHumanTurns = 0;
    let lastCompletedTurnHadUserMessage = false;
    for (const entry of branch) {
        if (!isSessionEntryWithType(entry))
            continue;
        if (entry.type === 'turn_start') {
            currentTurnHasUserMessage = false;
            continue;
        }
        if (isUserMessageEntry(entry)) {
            currentTurnHasUserMessage = true;
            continue;
        }
        if (entry.type === 'turn_end') {
            lastCompletedTurnHadUserMessage = currentTurnHasUserMessage;
            if (currentTurnHasUserMessage) {
                completedHumanTurns++;
            }
            currentTurnHasUserMessage = false;
        }
    }
    return {
        completedHumanTurns,
        lastCompletedTurnHadUserMessage
    };
}
/** Return the count of completed human turns derived from the session branch. */
function getCompletedHumanTurnCount(ctx) {
    return getHumanTurnState(ctx).completedHumanTurns;
}
/** Deep-compare two durable-memory workspace snapshots for equality. */
function snapshotsEqual(left, right) {
    if (left.index !== right.index)
        return false;
    if (left.topics.length !== right.topics.length)
        return false;
    return left.topics.every((topic, index) => {
        const other = right.topics[index];
        return topic.fileName === other?.fileName && topic.content === other.content;
    });
}
export class DurableMemoryCoordinator {
    workspace;
    activeBackgroundRun;
    pendingBackgroundRun;
    /** Construct the coordinator with an optional pre-built durable-memory workspace (for tests). */
    constructor(workspace = new DurableMemoryWorkspace()) {
        this.workspace = workspace;
    }
    /** Ensure the durable-memory workspace exists on disk. */
    initialize() {
        this.workspace.initialize();
    }
    /** Return the durable-memory index fragment to inject into the agent's system prompt. */
    getPromptFragment() {
        return buildDurableMemoryPromptFragment(this.workspace.readIndex());
    }
    /** Build the post-turn handler that schedules durable-memory checkpoints on cadence. */
    createPostTurnHandler() {
        return {
            name: 'durable-memory',
            priority: 250,
            parallel: false,
            shouldRun: (ctx) => {
                const { completedHumanTurns, lastCompletedTurnHadUserMessage } = getHumanTurnState(ctx);
                return (lastCompletedTurnHadUserMessage &&
                    completedHumanTurns > 0 &&
                    completedHumanTurns % DURABLE_MEMORY_TURN_END_CADENCE === 0);
            },
            execute: async (ctx, signal) => {
                if (signal.aborted)
                    return;
                const run = this.buildRunContext(ctx);
                if (!run)
                    return;
                this.enqueueCheckpoint({
                    context: run,
                    checkpoint: buildCheckpoint('turn_end', ctx.sessionManager.getSessionId(), getTurnTraceRef(ctx))
                });
            }
        };
    }
    /** Hook called before message compaction; flushes pending checkpoints and runs a pre-compact one. */
    async onBeforeCompaction(messagesToSummarize, ctx, signal) {
        if (messagesToSummarize.length === 0)
            return;
        await this.waitForBackgroundCheckpointDrain();
        const run = this.buildRunContext(ctx);
        if (!run)
            return;
        await this.runCheckpoint(run, buildCheckpoint('pre_compact', ctx.sessionManager.getSessionId(), `session:${ctx.sessionManager.getSessionId()}#pre_compact`), signal);
    }
    /** Begin a user-initiated durable-memory scan; returns false if no model context is available. */
    startManualScan(ctx, manualRequest) {
        this.initialize();
        const run = this.buildRunContext(ctx, manualRequest);
        if (!run)
            return false;
        this.enqueueCheckpoint({
            context: run,
            checkpoint: buildCheckpoint('turn_end', ctx.sessionManager.getSessionId(), `session:${ctx.sessionManager.getSessionId()}#manual_memory_scan`)
        });
        return true;
    }
    /** Capture the per-run context (model, paths, trimmed messages) needed for a checkpoint run. */
    buildRunContext(ctx, manualRequest) {
        const currentModel = ctx.model;
        if (!currentModel)
            return null;
        // `ExtensionContext.modelRegistry` is typed as the neutral
        // `AgentModelRegistry` interface, but every production path hands
        // in the concrete `ModelRegistry` (which exposes `authStorage`).
        // Cast here without a null check — tests mock a minimal registry
        // and `createShortcutAuthRecovery(undefined)` is well-defined.
        return {
            model: currentModel,
            modelRegistry: ctx.modelRegistry,
            sessionId: ctx.sessionManager.getSessionId(),
            sessionDir: ctx.sessionManager.getSessionDir(),
            sessionFile: ctx.sessionManager.getSessionFile?.(),
            manualRequest: manualRequest?.trim() || undefined,
            messages: toConversationMessages(ctx.getMessages())
        };
    }
    /** Either start the checkpoint immediately or replace any pending one when a run is in flight. */
    enqueueCheckpoint(run) {
        if (this.activeBackgroundRun) {
            // Background memory is periodic. If scans pile up, only the newest
            // checkpoint is useful because it sees the latest conversation branch.
            this.pendingBackgroundRun = run;
            return;
        }
        this.startBackgroundCheckpoint(run);
    }
    /** Run the next checkpoint in the background, chaining to any pending run when finished. */
    startBackgroundCheckpoint(run) {
        this.activeBackgroundRun = this.runCheckpoint(run.context, run.checkpoint, new AbortController().signal)
            .catch(() => { })
            .finally(() => {
            this.activeBackgroundRun = undefined;
            if (!this.pendingBackgroundRun) {
                return;
            }
            const next = this.pendingBackgroundRun;
            this.pendingBackgroundRun = undefined;
            this.startBackgroundCheckpoint(next);
        });
    }
    /** Await any active background checkpoint until the queue is fully drained. */
    async waitForBackgroundCheckpointDrain() {
        while (this.activeBackgroundRun) {
            await this.activeBackgroundRun;
        }
    }
    /** Execute one durable-memory checkpoint via the forked agent and emit the resulting status. */
    async runCheckpoint(run, checkpoint, signal) {
        if (signal.aborted)
            return;
        const memoryModel = run.modelRegistry.find?.(run.model.provider, DURABLE_MEMORY_AGENT_MODEL) ?? run.model;
        const baseUrl = memoryModel.baseUrl ?? run.model.baseUrl ?? SHORTCUT_LLM_PROXY_URL;
        const apiKey = await run.modelRegistry.getApiKey(memoryModel);
        if (!apiKey)
            return;
        const agent = durableMemoryAgent({
            checkpoint,
            workspaceRoot: this.workspace.getRootDir(),
            sessionDir: run.sessionDir,
            sessionFile: run.sessionFile,
            manualRequest: run.manualRequest,
            indexContent: this.workspace.readIndex(),
            topicFiles: this.workspace.listTopicFiles()
        });
        const tools = createDurableMemoryTools(this.workspace.getRootDir(), {
            readRoots: [run.sessionDir]
        });
        const permissionPolicy = deriveModePermissionPolicy(DURABLE_MEMORY_AGENT_TOOLS, undefined, [
            this.workspace.getRootDir(),
            run.sessionDir
        ]);
        const snapshot = this.workspace.snapshot();
        emitDurableMemoryStatus({ phase: 'running' });
        let agentResponse;
        try {
            const result = await runForkedAgent({
                systemPrompt: agent.systemPrompt,
                messages: run.messages,
                tools,
                model: memoryModel,
                baseUrl,
                apiKey,
                getExtraHeaders: () => ({ 'X-Session-Id': run.sessionId }),
                recoverAuth: createShortcutAuthRecovery(run.modelRegistry.authStorage),
                getToolContext: () => ({ permissionPolicy })
            }, {
                kind: DURABLE_MEMORY_FORK_KIND,
                maxTurns: DURABLE_MEMORY_FORK_MAX_TURNS,
                instruction: MEMORY_AGENT_INSTRUCTION,
                traceDir: resolveForkTraceDir(run.sessionDir, run.sessionFile),
                signal,
                effortLevel: 'low',
                onToolCall: emitRunningToolCall
            });
            agentResponse = summarizeAgentResponse(result.response);
        }
        catch {
            this.workspace.restore(snapshot);
            emitDurableMemoryStatus({ phase: 'error', message: 'Checkpoint failed.' });
            return;
        }
        const validation = validateWorkspaceState(this.workspace);
        if (validation.error) {
            this.workspace.restore(snapshot);
            emitDurableMemoryStatus({ phase: 'error', message: validation.error });
            return;
        }
        const nextSnapshot = this.workspace.snapshot();
        emitDurableMemoryStatus({
            phase: snapshotsEqual(snapshot, nextSnapshot) ? 'checked' : 'updated',
            message: agentResponse
        });
    }
}
//# sourceMappingURL=coordinator.js.map