"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionStore = exports.buildImageRefs = exports.SOURCE_PLAN_OUTPUT_SCHEMA = exports.SCAFFOLD_LEASE_TTL_MS = exports.DEFAULT_LEASE_TTL_MS = void 0;
const crypto_1 = require("crypto");
const errors_1 = require("./errors");
const contracts_1 = require("./contracts");
const sourceContext_1 = require("./sourceContext");
const DEFAULT_VARIANT_COUNT = 4;
const MAX_VARIANT_COUNT = 8;
/** TTL for a lease on a code_gen work item. Bumped to 10min after observing
 *  agents legitimately take longer than 3min when running 4+ Task sub-agents
 *  in parallel — the recovery path (re-lease + re-report) worked, but the UX
 *  delay before the chip's pills lit up was noticeable. */
exports.DEFAULT_LEASE_TTL_MS = 10 * 60 * 1000;
/** TTL for a lease on a scaffold_base work item. Slightly longer than
 *  code_gen because npm/yarn create can be slow on a cold cache. */
exports.SCAFFOLD_LEASE_TTL_MS = 15 * 60 * 1000;
const SYSTEM_CLOCK = { now: () => Date.now() };
function fingerprint(parts) {
    return parts.map((p) => (p === undefined ? '∅' : String(p))).join('|');
}
function leaseTtlForKind(kind) {
    return kind === 'scaffold_base'
        ? exports.SCAFFOLD_LEASE_TTL_MS
        : exports.DEFAULT_LEASE_TTL_MS;
}
/** Describes the work item contract returned to an agent lease holder. */
function descriptorFor(item) {
    return {
        id: item.id,
        kind: item.kind,
        attempt: item.attempt,
        dependsOn: [...item.dependsOn],
        input: item.input,
        output_schema: outputSchemaFor(item.kind),
    };
}
exports.SOURCE_PLAN_OUTPUT_SCHEMA = {
    sourceIntent: "sources: Array<{ url? | imageRef?, role: design_source|interaction_reference|asset_source|content_reference|other, confidence: low|medium|high, reason }> — classify every input source. Each entry keys by EXACTLY ONE of `url` (an inspiration URL the user provided) OR `imageRef` (a stable `image:<index>` reference for an attached-image artifact; the index matches the order of image-kind entries in the session's `sourceArtifacts`, i.e. visualBrief/screenshotNotes). Classify by prompt intent only, no deterministic URL heuristics.",
    sourceContext: 'sourceFindings, sectionInventory, visualObservations, sourceRoles, qualityBar, screenshotReferences?, risks?',
    designContexts: 'Array<{ url? | imageRef?, label, markdown }> REQUIRED for every design_source from sourceIntent. Use `url` for browser-extracted DESIGN.md from inspiration URLs; use `imageRef` (the matching `image:<index>` ref) for DESIGN.md you derived from an attached image. For image-derived DESIGN.md, write `Not observed` for sections you cannot determine from the image (commonly Responsive Behavior, full interaction states) rather than inventing values.',
    executionPlan: "{ mode: 'static_preview' | 'vite_app', confidence: low|medium|high, reason, assetPlan?: Array<{source, destination, referenceAs}>, runtimeRequirements?, userQuestion? } — choose vite_app for large local assets, model-viewer, Three.js, route structure, package deps, or files under public/; assetPlan.source must live under the approved asset root; static_preview for self-contained HTML/CSS/JS prototypes.",
};
/** Returns the completion payload shape required for each leased work item. */
function outputSchemaFor(kind) {
    switch (kind) {
        case 'source_plan':
            return exports.SOURCE_PLAN_OUTPUT_SCHEMA;
        case 'brief':
            return { briefs: 'Array<{briefId, label, body, visualReferenceUrl?}>' };
        case 'scaffold_base':
            return { workspacePath: 'string', framework: 'string' };
        case 'static_preview':
            return { html: 'string', css: 'string?', js: 'string?' };
        case 'refine_variant':
            return { html: 'string', css: 'string?', js: 'string?' };
        case 'code_gen':
            return { worktreePath: 'string', changedFiles: 'string[]' };
        case 'cleanup_runtime':
            return { released: 'boolean' };
    }
}
const variantDesignFromInput = (entry) => {
    if (!entry)
        return undefined;
    if (entry.kind === 'slug') {
        return {
            kind: 'slug',
            slug: entry.slug,
            displayName: entry.slug,
        };
    }
    return {
        kind: 'markdown',
        label: entry.label,
    };
};
const createBriefWorkItem = (args) => ({
    id: (0, crypto_1.randomUUID)(),
    kind: 'brief',
    status: 'pending',
    attempt: 0,
    dependsOn: [],
    input: {
        prompt: args.prompt,
        count: args.count,
        target: args.target,
        projectContext: args.projectContext,
    },
    acceptedReports: [],
});
// Source-artifact kinds that represent agent-described attached images.
// These need their own DESIGN.md slot in the source_plan output, alongside
// any URL-keyed entries. `siteBrief` / `urlText` are pasted text — they
// feed SOURCE_CONTEXT only and do NOT get a DESIGN.md.
const IMAGE_ARTIFACT_KINDS = new Set([
    'visualBrief',
    'screenshotNotes',
]);
/**
 * Stable per-session ref for image-derived design sources. Indexed over the
 * filtered list of image-kind artifacts so the ref survives reordering of
 * text-only artifacts in the same `sourceArtifacts` array.
 */
const buildImageRefs = (artifacts) => {
    if (!artifacts)
        return [];
    const refs = [];
    for (const artifact of artifacts) {
        if (IMAGE_ARTIFACT_KINDS.has(artifact.kind)) {
            refs.push(`image:${refs.length}`);
        }
    }
    return refs;
};
exports.buildImageRefs = buildImageRefs;
/**
 * Source-grounded fresh sessions require a single `source_plan` planning hop
 * before briefs. The hop fires when there's any source to classify — URLs
 * or attached images. Sessions with the plan already materialized via
 * `artifact` skip straight to `awaiting_briefs`.
 */
const requiresSourcePlan = (projectContext) => {
    if (projectContext.kind !== 'fresh')
        return false;
    const sourceContext = projectContext.sourceContext;
    if (!sourceContext)
        return false;
    const hasUrls = (sourceContext.sourceUrls?.length ?? 0) > 0;
    const hasImages = (0, exports.buildImageRefs)(sourceContext.sourceArtifacts).length > 0;
    if (!hasUrls && !hasImages)
        return false;
    const hasArtifact = Boolean(sourceContext.artifact);
    const hasIntent = Boolean(sourceContext.sourceIntent);
    const hasExecutionPlan = Boolean(projectContext.executionPlan);
    return !(hasArtifact && hasIntent && hasExecutionPlan);
};
class SessionStore {
    sessions = new Map();
    clock;
    constructor(clock = SYSTEM_CLOCK) {
        this.clock = clock;
    }
    // --- Lifecycle ----------------------------------------------------------
    propose(args) {
        const sessionId = args.sessionId ?? (0, crypto_1.randomUUID)();
        if (this.sessions.has(sessionId)) {
            throw new errors_1.AgentVariantsError('INVALID_TRANSITION', `Session ${sessionId} already exists`);
        }
        const projectContext = args.projectContext ?? {
            kind: 'existing',
        };
        const count = clampCount(args.count);
        const sourceContextForPlan = projectContext.kind === 'fresh'
            ? projectContext.sourceContext
            : undefined;
        const needsSourcePlan = requiresSourcePlan(projectContext);
        const initialItem = needsSourcePlan
            ? {
                id: (0, crypto_1.randomUUID)(),
                kind: 'source_plan',
                status: 'pending',
                attempt: 0,
                dependsOn: [],
                input: {
                    prompt: args.prompt,
                    count,
                    target: args.target,
                    sourceUrls: sourceContextForPlan?.sourceUrls ?? [],
                    sourceArtifacts: sourceContextForPlan?.sourceArtifacts ?? [],
                    preserveBrand: sourceContextForPlan?.preserveBrand ?? false,
                    projectContext,
                },
                acceptedReports: [],
            }
            : createBriefWorkItem({
                prompt: args.prompt,
                count,
                target: args.target,
                projectContext,
            });
        const record = {
            sessionId,
            stage: needsSourcePlan ? 'awaiting_source_plan' : 'awaiting_briefs',
            prompt: args.prompt,
            count,
            target: args.target,
            projectContext,
            briefs: [],
            approvedBriefs: [],
            workItems: new Map([[initialItem.id, initialItem]]),
            workItemOrder: [initialItem.id],
            acceptedFingerprints: new Map(),
            createdAt: this.clock.now(),
        };
        this.sessions.set(sessionId, record);
        return needsSourcePlan
            ? {
                sessionId,
                stage: 'awaiting_source_plan',
                sourcePlanWorkItem: initialItem,
            }
            : {
                sessionId,
                stage: 'awaiting_briefs',
                briefWorkItem: initialItem,
            };
    }
    reportBriefs(args) {
        const session = this.requireSession(args.sessionId);
        // Idempotent replay: accept regardless of current stage so retries after a
        // stage advance still return the same response without mutating state.
        const fp = fingerprint([
            'reportBriefs',
            args.sessionId,
            args.workItemId,
            args.leaseId,
            args.attempt,
            'succeeded',
        ]);
        const cached = session.acceptedFingerprints.get(fp);
        if (cached)
            return cached;
        this.assertStage(session, ['awaiting_briefs']);
        const item = this.requireWorkItem(session, args.workItemId);
        if (item.kind !== 'brief') {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Work item ${args.workItemId} is not a brief item`);
        }
        if (args.leaseId !== undefined) {
            this.validateLease(item, args.leaseId, args.attempt);
        }
        else if (item.attempt !== args.attempt) {
            throw new errors_1.AgentVariantsError('STALE_LEASE', `attempt mismatch: expected ${item.attempt}, got ${args.attempt}`);
        }
        item.status = 'succeeded';
        item.output = { briefs: args.briefs };
        session.briefs = args.briefs;
        session.stage = 'awaiting_approval';
        const result = { briefs: args.briefs };
        session.acceptedFingerprints.set(fp, result);
        return result;
    }
    reportSourcePlan(args) {
        const session = this.requireSession(args.sessionId);
        const fp = fingerprint([
            'reportSourcePlan',
            args.sessionId,
            args.workItemId,
            args.leaseId,
            args.attempt,
            'succeeded',
        ]);
        const cached = session.acceptedFingerprints.get(fp);
        if (cached)
            return cached;
        this.assertStage(session, ['awaiting_source_plan']);
        const item = this.requireWorkItem(session, args.workItemId);
        if (item.kind !== 'source_plan') {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Work item ${args.workItemId} is not a source_plan item`);
        }
        this.validateLease(item, args.leaseId, args.attempt);
        const { sourceIntent, sourceContext, designContexts, executionPlan } = args.sourcePlan;
        // Cross-check: every source URL AND every image-derived source on the
        // session must be classified, and every design_source (URL or image)
        // must come with a DESIGN.md.
        const sessionSourceUrls = session.projectContext.kind === 'fresh'
            ? (session.projectContext.sourceContext?.sourceUrls ?? [])
            : [];
        const sessionImageRefs = session.projectContext.kind === 'fresh'
            ? (0, exports.buildImageRefs)(session.projectContext.sourceContext?.sourceArtifacts)
            : [];
        const classifiedUrls = new Set(sourceIntent.sources
            .map((e) => e.url)
            .filter((u) => Boolean(u)));
        const classifiedImageRefs = new Set(sourceIntent.sources
            .map((e) => e.imageRef)
            .filter((r) => Boolean(r)));
        const missingClassification = [
            ...sessionSourceUrls.filter((url) => !classifiedUrls.has(url)),
            ...sessionImageRefs.filter((ref) => !classifiedImageRefs.has(ref)),
        ];
        if (missingClassification.length > 0) {
            throw new errors_1.AgentVariantsError('SOURCE_CONTEXT_INVALID', `source_plan missing sourceIntent classification for: ${missingClassification.join(', ')}`);
        }
        const designSourceUrls = sourceIntent.sources
            .filter((entry) => entry.role === 'design_source' && Boolean(entry.url))
            .map((entry) => entry.url);
        const designSourceImageRefs = sourceIntent.sources
            .filter((entry) => entry.role === 'design_source' && Boolean(entry.imageRef))
            .map((entry) => entry.imageRef);
        const reportedDesignUrls = new Set((designContexts ?? [])
            .map((entry) => entry.url)
            .filter((u) => Boolean(u)));
        const reportedDesignImageRefs = new Set((designContexts ?? [])
            .map((entry) => entry.imageRef)
            .filter((r) => Boolean(r)));
        const missingDesignContexts = [
            ...designSourceUrls.filter((url) => !reportedDesignUrls.has(url)),
            ...designSourceImageRefs.filter((ref) => !reportedDesignImageRefs.has(ref)),
        ];
        if (missingDesignContexts.length > 0) {
            throw new errors_1.AgentVariantsError('SOURCE_CONTEXT_INVALID', `Missing DESIGN.md for design source(s): ${missingDesignContexts.join(', ')}`);
        }
        const normalizedArtifact = (0, sourceContext_1.normalizeSourceContextArtifact)(sourceContext);
        if (session.projectContext.kind === 'fresh') {
            // Persist one designContext entry PER design_source URL, aligned
            // to the SOURCE_INTENT design_source order (NOT the agent's
            // designContexts submission order). Brief fan-out
            // (provisionFreshWorktrees) reads slot [min(briefIndex, length-1)]
            // and falls back to slot 0 — a single-source session still ships
            // one shared markdown, but a multi-source session puts each
            // design_source's DESIGN.md at a deterministic slot regardless of
            // how many non-design_source entries the agent classified or the
            // order it shipped designContexts in. Without this alignment, an
            // extra interaction_reference URL ahead of a design_source in the
            // agent's payload would push the wrong DESIGN.md onto variant 0.
            const designContextByUrl = new Map();
            const designContextByImageRef = new Map();
            for (const entry of designContexts ?? []) {
                const payload = { label: entry.label, markdown: entry.markdown };
                if (entry.url) {
                    designContextByUrl.set(entry.url, payload);
                }
                else if (entry.imageRef) {
                    designContextByImageRef.set(entry.imageRef, payload);
                }
            }
            // Persist URL-keyed entries first (preserving the URL design_source
            // order from sourceIntent), then image-keyed entries (preserving
            // their design_source order). Brief fan-out reads slot
            // [min(briefIndex, length-1)] with a slot-0 fallback, so this
            // ordering puts URL inspirations on the early variant slots when
            // both URLs and images are present.
            const alignedDesignContexts = [
                ...designSourceUrls
                    .map((url) => designContextByUrl.get(url))
                    .filter((entry) => Boolean(entry)),
                ...designSourceImageRefs
                    .map((ref) => designContextByImageRef.get(ref))
                    .filter((entry) => Boolean(entry)),
            ];
            session.projectContext = {
                ...session.projectContext,
                ...(alignedDesignContexts.length > 0
                    ? {
                        designContext: alignedDesignContexts.map((entry) => ({
                            kind: 'markdown',
                            label: entry.label,
                            content: entry.markdown,
                        })),
                    }
                    : {}),
                sourceContext: {
                    ...(session.projectContext.sourceContext ?? {}),
                    sourceIntent,
                    artifact: normalizedArtifact,
                },
                executionPlan,
            };
        }
        item.status = 'succeeded';
        item.output = {
            sourceIntent,
            sourceContext: normalizedArtifact,
            ...(designContexts ? { designContexts } : {}),
            executionPlan,
        };
        item.lease = undefined;
        const briefItem = createBriefWorkItem({
            prompt: session.prompt,
            count: session.count,
            target: session.target,
            projectContext: session.projectContext,
        });
        session.workItems.set(briefItem.id, briefItem);
        session.workItemOrder.push(briefItem.id);
        session.stage = 'awaiting_briefs';
        const result = { briefWorkItem: briefItem };
        session.acceptedFingerprints.set(fp, result);
        return result;
    }
    approve(args) {
        const session = this.requireSession(args.sessionId);
        this.assertStage(session, ['awaiting_approval']);
        if (args.selections.length === 0) {
            throw new errors_1.AgentVariantsError('MISSING_REQUIRED_INPUT', 'At least one brief must be approved');
        }
        const briefIds = new Set(session.briefs.map((b) => b.briefId));
        for (const sel of args.selections) {
            if (!briefIds.has(sel.briefId)) {
                throw new errors_1.AgentVariantsError('MISSING_REQUIRED_INPUT', `Unknown briefId: ${sel.briefId}`);
            }
        }
        session.approvedBriefs = args.selections;
        const isFresh = session.projectContext.kind === 'fresh';
        const executionPlan = session.projectContext.kind === 'fresh'
            ? session.projectContext.executionPlan
            : undefined;
        // Surface an agent-flagged clarification as a hard blocker rather than
        // silently downgrading to a static preview. The agent sets
        // `userQuestion` when it cannot decide between static_preview and
        // vite_app without more info from the user.
        if (executionPlan?.userQuestion) {
            throw new errors_1.AgentVariantsError('MISSING_REQUIRED_INPUT', executionPlan.userQuestion);
        }
        // Fresh sessions default to static_preview when no plan was reported
        // (source-less zero-to-one). Source-grounded sessions carry an
        // executionPlan from `report_source_plan`.
        const freshMode = isFresh
            ? (executionPlan?.mode ?? 'static_preview')
            : 'static_preview';
        const needsViteApp = isFresh && freshMode === 'vite_app';
        const codeGenIds = [];
        // For fresh+vite_app sessions, code_gen items depend on a single
        // scaffold_base item so the orchestrator can do the Vite skeleton +
        // npm install + asset copy before the agent leases the code_gen.
        let scaffoldBaseId;
        if (needsViteApp) {
            scaffoldBaseId = (0, crypto_1.randomUUID)();
            const scaffoldItem = {
                id: scaffoldBaseId,
                kind: 'scaffold_base',
                status: 'pending',
                attempt: 0,
                dependsOn: [],
                input: { projectContext: session.projectContext },
                acceptedReports: [],
                internal: true,
            };
            session.workItems.set(scaffoldItem.id, scaffoldItem);
            session.workItemOrder.push(scaffoldItem.id);
        }
        args.selections.forEach((sel) => {
            const briefIndex = session.briefs.findIndex((b) => b.briefId === sel.briefId);
            if (briefIndex === -1)
                return;
            const brief = session.briefs[briefIndex];
            // D-VR2: Plumb the optional per-brief visualReferenceUrl into the
            // resulting work-item input. Omitted (not nulled) when absent so the
            // shape stays backward-compatible for callers that destructure.
            const visualRef = brief.visualReferenceUrl
                ? { visualReferenceUrl: brief.visualReferenceUrl }
                : {};
            if (isFresh && !needsViteApp) {
                // Fresh + static_preview: HTML is the deliverable. No scaffold or
                // code_gen — the static_preview item is leased directly.
                const staticPreviewItem = {
                    id: (0, crypto_1.randomUUID)(),
                    kind: 'static_preview',
                    status: 'pending',
                    attempt: 0,
                    dependsOn: [],
                    input: {
                        briefId: brief.briefId,
                        briefLabel: brief.label,
                        briefBody: sel.bodyOverride ?? brief.body,
                        ...visualRef,
                        projectContext: session.projectContext,
                    },
                    acceptedReports: [],
                };
                session.workItems.set(staticPreviewItem.id, staticPreviewItem);
                session.workItemOrder.push(staticPreviewItem.id);
            }
            else {
                // Existing project OR fresh+vite_app: code_gen against a worktree.
                // Fresh+vite_app additionally depends on scaffold_base so the agent
                // doesn't lease code_gen before the Vite skeleton lands.
                //
                // Per-slot designContextEntry is read by provisionFreshWorktrees to
                // decide what DESIGN.md to write per variant. Falls back to slot 0
                // when the session has fewer designContext entries than variants
                // (the common case after report_source_plan ships one shared
                // markdown).
                const freshDesignContext = session.projectContext.kind === 'fresh'
                    ? session.projectContext.designContext
                    : undefined;
                const designContextEntry = freshDesignContext
                    ? freshDesignContext[Math.min(briefIndex, freshDesignContext.length - 1)]
                    : undefined;
                const codeGenItem = {
                    id: (0, crypto_1.randomUUID)(),
                    kind: 'code_gen',
                    status: 'pending',
                    attempt: 0,
                    dependsOn: scaffoldBaseId ? [scaffoldBaseId] : [],
                    input: {
                        briefId: brief.briefId,
                        briefLabel: brief.label,
                        briefBody: sel.bodyOverride ?? brief.body,
                        ...visualRef,
                        target: session.target,
                        projectContext: session.projectContext,
                        ...(designContextEntry ? { designContextEntry } : {}),
                    },
                    acceptedReports: [],
                };
                session.workItems.set(codeGenItem.id, codeGenItem);
                session.workItemOrder.push(codeGenItem.id);
                codeGenIds.push(codeGenItem.id);
            }
        });
        session.stage = 'work_items_ready';
        return {
            approvedCount: args.selections.length,
            totalCount: session.briefs.length,
            codeGenWorkItemIds: codeGenIds,
            scaffoldBaseWorkItemId: scaffoldBaseId,
        };
    }
    // --- Lease management ---------------------------------------------------
    requestWork(args) {
        const session = this.requireSession(args.sessionId);
        this.assertStage(session, [
            'awaiting_source_plan',
            'work_items_ready',
            'waiting_for_results',
        ]);
        this.expireStaleLeases(session);
        let leasable = this.leasableItems(session);
        // Targeted lease: restrict to a single named item so callers that just
        // enqueued a specific work item (e.g. refineVariant) can't accidentally
        // lease an unrelated leasable item that happens to sort earlier.
        if (args.workItemId !== undefined) {
            const target = leasable.find((item) => item.id === args.workItemId);
            if (!target) {
                throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Work item ${args.workItemId} is not currently leasable (running, blocked by deps, or done)`);
            }
            leasable = [target];
        }
        const limit = args.requestedLeaseCount ?? leasable.length;
        const issued = leasable.slice(0, Math.max(0, limit));
        if (issued.length === 0) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', 'No work items currently leasable (all running, blocked by deps, or done)');
        }
        const now = this.clock.now();
        const leaseId = (0, crypto_1.randomUUID)();
        for (const item of issued) {
            const ttl = leaseTtlForKind(item.kind);
            item.attempt = item.attempt + 1;
            item.lease = {
                leaseId,
                leaseOwner: args.leaseOwner,
                leaseIssuedAt: now,
                leaseExpiresAt: now + ttl,
                attempt: item.attempt,
            };
            item.status = 'running';
        }
        session.stage =
            session.stage === 'awaiting_source_plan'
                ? 'awaiting_source_plan'
                : 'waiting_for_results';
        return {
            leaseId,
            leaseTtlMs: Math.max(...issued.map((i) => leaseTtlForKind(i.kind))),
            leasedWorkItems: issued.map((item) => descriptorFor(item)),
        };
    }
    reportComplete(args) {
        const session = this.requireSession(args.sessionId);
        if (contracts_1.TERMINAL_STAGES.has(session.stage)) {
            // idempotent: terminal sessions accept replays of accepted fingerprints,
            // reject new mutating reports.
            const fp = fingerprint([
                'reportComplete',
                args.sessionId,
                args.workItemId,
                args.leaseId,
                args.attempt,
                args.status,
            ]);
            const cached = session.acceptedFingerprints.get(fp);
            if (cached)
                return cached;
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Session is in terminal stage ${session.stage}; new reports rejected`);
        }
        this.assertStage(session, ['work_items_ready', 'waiting_for_results']);
        const item = this.requireWorkItem(session, args.workItemId);
        const fp = fingerprint([
            'reportComplete',
            args.sessionId,
            args.workItemId,
            args.leaseId,
            args.attempt,
            args.status,
        ]);
        const cached = session.acceptedFingerprints.get(fp);
        if (cached)
            return cached;
        this.validateLease(item, args.leaseId, args.attempt);
        if (args.status === 'running') {
            // mid-status heartbeat: extend lease, return progress.
            const now = this.clock.now();
            if (item.lease) {
                item.lease.leaseExpiresAt = now + leaseTtlForKind(item.kind);
            }
            return {
                stage: session.stage,
                progress: this.progressOf(session),
            };
        }
        item.status = args.status;
        item.output = args.output;
        item.error = args.error;
        item.acceptedReports.push(fp);
        item.lease = undefined;
        if (args.status === 'failed') {
            this.cascadeDependencyFailure(session, item.id);
        }
        else if (args.status === 'succeeded') {
            // succeeded items unblock dependents — handled in leasableItems()
        }
        const result = this.evaluateTerminal(session);
        session.acceptedFingerprints.set(fp, result);
        return result;
    }
    /**
     * Creates a refinable HTML work item for a completed static preview variant.
     * The target variant remains the user-facing snapshot; this work item only
     * tracks the in-flight refinement and is surfaced through its `refinement`.
     */
    createRefineVariantWorkItem(args) {
        const session = this.requireSession(args.sessionId);
        const target = this.requireWorkItem(session, args.variantId);
        if (target.kind !== 'static_preview' || target.status !== 'succeeded') {
            throw new errors_1.AgentVariantsError('VARIANT_NOT_EDITABLE', 'Only completed static_preview variants can be refined');
        }
        if (session.variantPick) {
            throw new errors_1.AgentVariantsError('PENDING_CHANGE_CONFLICT', 'Cannot refine a variant after one has already been sent');
        }
        const activeRefinement = [...session.workItems.values()].find((item) => {
            if (item.kind !== 'refine_variant')
                return false;
            const input = item.input;
            return input.variantId === args.variantId && item.status === 'pending';
        });
        if (activeRefinement) {
            throw new errors_1.AgentVariantsError('PENDING_CHANGE_CONFLICT', 'A refinement is already queued for this variant');
        }
        const targetInput = target.input;
        const item = {
            id: (0, crypto_1.randomUUID)(),
            kind: 'refine_variant',
            status: 'pending',
            attempt: 0,
            dependsOn: [],
            input: {
                variantId: args.variantId,
                instruction: args.instruction,
                briefId: targetInput.briefId,
                briefLabel: targetInput.briefLabel,
                briefBody: targetInput.briefBody,
                ...(args.currentHtml ? { currentHtml: args.currentHtml } : {}),
                projectContext: targetInput.projectContext ?? session.projectContext,
            },
            acceptedReports: [],
        };
        session.workItems.set(item.id, item);
        session.workItemOrder.push(item.id);
        session.stage = 'work_items_ready';
        return item;
    }
    /**
     * Fold a newer instruction into a variant's still-pending refinement. Used
     * when a second refinement is requested for a variant that already has a
     * queued (not yet leased) refinement: the new instruction is appended so the
     * agent applies both rather than dropping the later request. Returns false
     * when no pending refinement exists.
     */
    appendPendingRefineVariantInstruction(args) {
        const session = this.requireSession(args.sessionId);
        const refineItem = [...session.workItems.values()].find((item) => {
            if (item.kind !== 'refine_variant')
                return false;
            const input = item.input;
            return input.variantId === args.variantId && item.status === 'pending';
        });
        if (!refineItem)
            return false;
        const input = refineItem.input;
        input.instruction =
            input.instruction && input.instruction.length > 0
                ? `${input.instruction}\n\n${args.instruction}`
                : args.instruction;
        return true;
    }
    /**
     * Lease the pending refinement already associated with a specific static
     * preview variant. Keeps multi-target refinement batches independent of
     * work-item order.
     */
    leaseRefineItemForVariant(args) {
        const session = this.requireSession(args.sessionId);
        const runningRefinement = [...session.workItems.values()].find((item) => {
            if (item.kind !== 'refine_variant')
                return false;
            const input = item.input;
            return input.variantId === args.variantId && item.status === 'running';
        });
        if (runningRefinement) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Variant ${args.variantId} already has a running refinement`);
        }
        const refineItem = [...session.workItems.values()].find((item) => {
            if (item.kind !== 'refine_variant')
                return false;
            const input = item.input;
            return input.variantId === args.variantId && item.status === 'pending';
        });
        if (!refineItem) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `No pending refinement exists for variant ${args.variantId}`);
        }
        return this.requestWork({
            sessionId: args.sessionId,
            leaseOwner: args.leaseOwner,
            workItemId: refineItem.id,
        });
    }
    /**
     * Mark a server-owned work item as succeeded without going through the
     * lease + agent-report path. Used for `scaffold_base` items where the
     * orchestrator runs the scaffold + npm install in the background and needs
     * to unblock dependent code_gen items when it's done.
     *
     * Bypasses lease validation because no lease was ever issued for internal
     * items. Idempotent: completing an already-succeeded item is a no-op.
     */
    completeInternal(args) {
        const session = this.requireSession(args.sessionId);
        const item = this.requireWorkItem(session, args.workItemId);
        if (!item.internal) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `completeInternal called on non-internal work item ${args.workItemId}`);
        }
        if (item.status === 'succeeded') {
            return this.evaluateTerminal(session);
        }
        item.status = 'succeeded';
        item.output = args.output;
        item.lease = undefined;
        return this.evaluateTerminal(session);
    }
    /**
     * Mark a server-owned work item as failed, cascading DEPENDENCY_FAILED to
     * its dependents. Used when the orchestrator's background scaffold work
     * (npm install, file writes) fails and we need the session to degrade.
     */
    failInternal(args) {
        const session = this.requireSession(args.sessionId);
        const item = this.requireWorkItem(session, args.workItemId);
        if (!item.internal) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `failInternal called on non-internal work item ${args.workItemId}`);
        }
        if (item.status === 'succeeded' || item.status === 'failed') {
            return this.evaluateTerminal(session);
        }
        item.status = 'failed';
        item.error = args.error;
        item.lease = undefined;
        this.cascadeDependencyFailure(session, item.id);
        return this.evaluateTerminal(session);
    }
    // --- Cancel / cleanup ---------------------------------------------------
    cancel(args) {
        const session = this.requireSession(args.sessionId);
        if (session.stage === 'cancelled') {
            return { stage: 'cancelled', cleanupStatus: 'complete' };
        }
        session.stage = 'cancelled';
        session.cancelReason = args.reason;
        for (const item of session.workItems.values()) {
            if (item.status === 'pending' || item.status === 'running') {
                item.status = 'cancelled';
                item.lease = undefined;
            }
        }
        // No cleanup_runtime items in Phase 1; status starts as pending until
        // Phase 2 wires the worktree teardown.
        return { stage: 'cancelled', cleanupStatus: 'pending' };
    }
    /**
     * Cancel a single work item without tearing down its sibling variants or
     * the session itself. Idempotent on terminal states — already-cancelled,
     * succeeded, or failed items return their current status with
     * `alreadyTerminal: true` and no state change. Triggers
     * `evaluateTerminal` so the session can reach a terminal stage if this
     * was the last in-flight item.
     */
    cancelWorkItem(args) {
        const session = this.requireSession(args.sessionId);
        const item = this.requireWorkItem(session, args.workItemId);
        if (item.status === 'cancelled' ||
            item.status === 'succeeded' ||
            item.status === 'failed') {
            return {
                workItemId: item.id,
                finalStatus: item.status,
                sessionStage: session.stage,
                alreadyTerminal: true,
            };
        }
        item.status = 'cancelled';
        item.lease = undefined;
        this.evaluateTerminal(session);
        return {
            workItemId: item.id,
            finalStatus: 'cancelled',
            sessionStage: session.stage,
            alreadyTerminal: false,
        };
    }
    // --- Variant pick handoff (queues to pending-changes adapter) -----------
    recordVariantPick(args) {
        const session = this.requireSession(args.sessionId);
        if (!['ready', 'degraded'].includes(session.stage)) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Cannot pick variant in stage ${session.stage}`);
        }
        if (session.variantPick &&
            session.variantPick.variantId === args.envelope.variantId) {
            return { duplicate: true };
        }
        if (session.variantPick) {
            throw new errors_1.AgentVariantsError('PENDING_CHANGE_CONFLICT', `Session already has a variant pick for ${session.variantPick.variantId}`);
        }
        session.variantPick = args.envelope;
        return { duplicate: false };
    }
    // --- Read accessors -----------------------------------------------------
    getStage(sessionId) {
        return this.requireSession(sessionId).stage;
    }
    getPrompt(sessionId) {
        return this.requireSession(sessionId).prompt;
    }
    getBriefs(sessionId) {
        return [...this.requireSession(sessionId).briefs];
    }
    getProgress(sessionId) {
        return this.progressOf(this.requireSession(sessionId));
    }
    getSummary(sessionId) {
        const session = this.requireSession(sessionId);
        return this.summaryOf(session);
    }
    getVariants(sessionId) {
        const session = this.requireSession(sessionId);
        return this.variantsOf(session);
    }
    getVariantPick(sessionId) {
        return this.requireSession(sessionId).variantPick;
    }
    /** Validates a report lease without mutating the work item. */
    validateReportLease(args) {
        const session = this.requireSession(args.sessionId);
        const item = this.requireWorkItem(session, args.workItemId);
        this.validateLease(item, args.leaseId, args.attempt);
    }
    hasSession(sessionId) {
        return this.sessions.has(sessionId);
    }
    /** Read a work item's input blob — used by the orchestrator to recover
     *  briefLabel / target / projectContext when auto-enqueueing the diff. */
    getWorkItemInput(sessionId, workItemId) {
        const session = this.requireSession(sessionId);
        const item = this.requireWorkItem(session, workItemId);
        return item.input;
    }
    getWorkItemOutput(sessionId, workItemId) {
        const session = this.requireSession(sessionId);
        const item = this.requireWorkItem(session, workItemId);
        return item.output;
    }
    /** Replaces a completed work item's output after server-side post-processing. */
    replaceWorkItemOutput(sessionId, workItemId, output) {
        const session = this.requireSession(sessionId);
        const item = this.requireWorkItem(session, workItemId);
        item.output = output;
    }
    /** Read a work item's `kind` discriminator (brief | source_plan |
     *  static_preview | code_gen | scaffold_base). Used by callers that need to
     *  branch their validation on the work-item shape — e.g. enforcing the
     *  inline-HTML contract on static_preview `succeeded` reports. */
    getWorkItemKind(sessionId, workItemId) {
        const session = this.requireSession(sessionId);
        const item = this.requireWorkItem(session, workItemId);
        return item.kind;
    }
    /** Read the projectContext for a session (existing | fresh). */
    getProjectContext(sessionId) {
        return this.requireSession(sessionId).projectContext;
    }
    // --- Internals ----------------------------------------------------------
    requireSession(sessionId) {
        const s = this.sessions.get(sessionId);
        if (!s) {
            throw new errors_1.AgentVariantsError('SESSION_NOT_FOUND', `Unknown sessionId: ${sessionId}`);
        }
        return s;
    }
    requireWorkItem(session, id) {
        const item = session.workItems.get(id);
        if (!item) {
            throw new errors_1.AgentVariantsError('WORK_ITEM_NOT_FOUND', `Unknown workItemId: ${id}`);
        }
        return item;
    }
    assertStage(session, allowed) {
        if (!allowed.includes(session.stage)) {
            throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Action not allowed in stage ${session.stage}; allowed: ${allowed.join(', ')}`);
        }
    }
    validateLease(item, leaseId, attempt) {
        if (!item.lease) {
            throw new errors_1.AgentVariantsError('LEASE_MISMATCH', `Work item ${item.id} has no active lease`);
        }
        if (item.lease.leaseId !== leaseId) {
            throw new errors_1.AgentVariantsError('STALE_LEASE', `Lease ${leaseId} does not match active lease`);
        }
        if (this.clock.now() > item.lease.leaseExpiresAt) {
            throw new errors_1.AgentVariantsError('LEASE_EXPIRED', `Lease for work item ${item.id} expired`);
        }
        if (item.lease.attempt !== attempt) {
            throw new errors_1.AgentVariantsError('STALE_LEASE', `attempt mismatch: lease has attempt ${item.lease.attempt}, got ${attempt}`);
        }
    }
    expireStaleLeases(session) {
        const now = this.clock.now();
        for (const item of session.workItems.values()) {
            if (item.lease &&
                item.status === 'running' &&
                now > item.lease.leaseExpiresAt) {
                item.lease = undefined;
                item.status = 'pending';
            }
        }
    }
    leasableItems(session) {
        const out = [];
        for (const id of session.workItemOrder) {
            const item = session.workItems.get(id);
            if (item.kind === 'brief')
                continue;
            // Server-owned items never surface to external agents.
            if (item.internal)
                continue;
            if (item.status !== 'pending')
                continue;
            if (!this.dependenciesMet(session, item))
                continue;
            if (item.kind === 'static_preview' &&
                this.hasLeasableCodeGenForStaticPreview(session, item)) {
                continue;
            }
            out.push(item);
        }
        return out;
    }
    hasLeasableCodeGenForStaticPreview(session, staticPreview) {
        const staticInput = staticPreview.input;
        if (!staticInput.briefId)
            return false;
        for (const candidate of session.workItems.values()) {
            if (candidate.kind !== 'code_gen')
                continue;
            if (candidate.status !== 'pending')
                continue;
            if (!this.dependenciesMet(session, candidate))
                continue;
            const codeInput = candidate.input;
            if (codeInput.briefId === staticInput.briefId)
                return true;
        }
        return false;
    }
    dependenciesMet(session, item) {
        for (const depId of item.dependsOn) {
            const dep = session.workItems.get(depId);
            if (!dep || dep.status !== 'succeeded')
                return false;
        }
        return true;
    }
    cascadeDependencyFailure(session, failedId) {
        for (const item of session.workItems.values()) {
            if (item.dependsOn.includes(failedId) && item.status === 'pending') {
                item.status = 'failed';
                item.error = {
                    code: 'DEPENDENCY_FAILED',
                    message: `Dependency ${failedId} failed`,
                };
            }
        }
    }
    terminalItems(session) {
        const kind = this.variantWorkItemKind(session);
        return [...session.workItems.values()].filter((i) => i.kind === kind);
    }
    /**
     * The work-item kind that represents a user-facing variant for this
     * session. Drives both progress accounting and the active variants
     * snapshot. Fresh+vite_app sessions use `code_gen` like existing
     * projects; fresh+static_preview (the default for source-less 0→1)
     * keeps `static_preview` as the deliverable.
     */
    variantWorkItemKind(session) {
        if (session.projectContext.kind !== 'fresh')
            return 'code_gen';
        const mode = session.projectContext.executionPlan?.mode ?? 'static_preview';
        return mode === 'vite_app' ? 'code_gen' : 'static_preview';
    }
    evaluateTerminal(session) {
        const activeRefinement = [...session.workItems.values()].find((item) => item.kind === 'refine_variant' &&
            (item.status === 'pending' || item.status === 'running'));
        if (activeRefinement) {
            return {
                stage: session.stage,
                progress: this.progressOf(session),
            };
        }
        const items = this.terminalItems(session);
        const allDone = items.every((i) => i.status === 'succeeded' ||
            i.status === 'failed' ||
            i.status === 'cancelled');
        if (!allDone) {
            return {
                stage: session.stage,
                progress: this.progressOf(session),
            };
        }
        const succeeded = items.filter((i) => i.status === 'succeeded').length;
        let terminal;
        if (succeeded === items.length)
            terminal = 'ready';
        else if (succeeded > 0)
            terminal = 'degraded';
        else
            terminal = 'failed';
        session.stage = terminal;
        return {
            stage: terminal,
            progress: this.progressOf(session),
            summary: this.summaryOf(session),
        };
    }
    progressOf(session) {
        if (session.stage === 'awaiting_source_plan') {
            const planItems = [...session.workItems.values()].filter((item) => item.kind === 'source_plan');
            const ready = planItems.filter((item) => item.status === 'succeeded').length;
            return { ready, total: planItems.length };
        }
        const items = this.terminalItems(session);
        const ready = items.filter((i) => i.status === 'succeeded').length;
        return { ready, total: items.length };
    }
    summaryOf(session) {
        const items = this.terminalItems(session);
        return {
            successCount: items.filter((i) => i.status === 'succeeded').length,
            failureCount: items.filter((i) => i.status === 'failed').length,
            cancelledCount: items.filter((i) => i.status === 'cancelled').length,
            variants: items.map((i) => {
                const input = i.input;
                return {
                    workItemId: i.id,
                    status: i.status,
                    label: input.briefLabel,
                };
            }),
        };
    }
    variantsOf(session) {
        const kind = this.variantWorkItemKind(session);
        const refinementsByVariantId = new Map();
        for (const item of session.workItems.values()) {
            if (item.kind !== 'refine_variant')
                continue;
            const input = item.input;
            if (!input.variantId)
                continue;
            refinementsByVariantId.set(input.variantId, item);
        }
        return [...session.workItems.values()]
            .filter((i) => i.kind === kind)
            .map((item) => {
            const input = item.input;
            return {
                workItemId: item.id,
                briefId: input.briefId ?? '',
                label: input.briefLabel ?? 'Variant',
                status: item.status,
                design: variantDesignFromInput(input.designContextEntry),
                ...(refinementsByVariantId.get(item.id)
                    ? {
                        refinement: {
                            workItemId: refinementsByVariantId.get(item.id).id,
                            status: refinementsByVariantId.get(item.id).status,
                            ...(refinementsByVariantId.get(item.id).error?.code
                                ? {
                                    errorCode: String(refinementsByVariantId.get(item.id).error?.code),
                                }
                                : {}),
                        },
                    }
                    : {}),
                ...(item.error?.code ? { errorCode: String(item.error.code) } : {}),
            };
        });
    }
}
exports.SessionStore = SessionStore;
function clampCount(count) {
    if (count === undefined)
        return DEFAULT_VARIANT_COUNT;
    if (count < 2)
        return 2;
    if (count > MAX_VARIANT_COUNT)
        return MAX_VARIANT_COUNT;
    return count;
}
//# sourceMappingURL=SessionStore.js.map