import { __awaiter } from "tslib";
import { OrchestratingDriver } from '../../components/orchestrating-driver/orchestrating-driver';
import { flattenSubAgentMessages } from '../../utils/flatten-sub-agent-messages';
import { logger } from '../../utils/logger';
import { isSessionNoticeMarker, SESSION_RESTORE_FAILED_LABEL, SESSION_RESTORED_LABEL, } from '../../utils/message-partition';
import { withTimeout } from '../../utils/with-timeout';
import { recordMetaEvent } from '../debug-event-log';
import { diagnosticsMessageKey } from './diagnostics';
import { getDiagnosticsCursors } from './diagnostics-cursors';
import { createPersistedSession, isRestorableSession, keepThroughLastUserMessage, reconcileAgentSnapshots, resolveNonResumableNotice, toLoadSessionPayload, trimToRestorableBoundary, unknownAgentSnapshots, } from './session-snapshot';
/**
 * Backstop for a slow/hung persistence provider on startup restore (GENC-1351
 * §6). Deliberately long: silently losing a restorable session is a far worse
 * outcome than a long "Restoring…" wait, so we give a slow backend plenty of room
 * and only give up (→ fresh session + a "couldn't restore" notice) when a load is
 * genuinely stuck. A healthy server responds well under this. Bounds both the
 * session `load` and the preferences load.
 */
const RESTORE_TIMEOUT_MS = 30000;
/**
 * Default throttle between diagnostics appends (GENC-1351 §5.8) when the provider
 * doesn't specify `diagnosticsDebounceMs`. Coarser than the restore-core save — the
 * debug log is heavy and doesn't need per-change freshness.
 */
const DEFAULT_DIAGNOSTICS_DEBOUNCE_MS = 4000;
/**
 * Fallback notice shown on return when an agent declared its state non-resumable
 * (GENC-1351 §5.9) and set no `notResumableMessage`. Paired with the composer
 * pre-filled with the flow-activating prompt, so "send it again" is one keystroke.
 */
const DEFAULT_NOT_RESUMABLE_MESSAGE = "Sorry, I didn't manage to finish the task from your previous message. Send it again if you'd like me to try it again";
/**
 * Owns **restore-core** persistence for one session (GENC-1351 §5.10) — the
 * save/restore/freeze of the {@link PersistedSession} snapshot plus the
 * carry-forward cache. Created once per session via `persister-registry.ts` and
 * shared by every element view, so there is a single save timer and a single
 * cache: the per-view double-write races (and the corruption they caused under a
 * non-atomic backend) are gone by construction.
 *
 * Owns all provider I/O for the session: the restore-core snapshot (save/restore/
 * freeze), the user-preferences blob (load/save — the element keeps only the
 * `chatConfig`-defaults reconciler), and the diagnostics-append scheduling (one
 * timer per session; entry-building stays element-owned via `getDiagnosticEntries`).
 *
 * @internal
 */
export class SessionPersister {
    constructor(deps) {
        this.deps = deps;
        this._lastSaveAt = 0;
        this._lastDiagnosticsAt = 0;
        /**
         * The flow-owner's last-known-good serialized image, carried forward when a
         * post-restore autosave fires before the agent re-activates (its live capture is
         * empty). Seeded on restore. See {@link reconcileAgentSnapshots}.
         */
        this._cache = {};
        /**
         * The loaded preferences blob (GENC-1351 §5.10 Phase 2), owned here so it's
         * **shared per session** — the element's `applyPreferenceBaseline` reconciler reads
         * it via {@link storedPreferences}. Kept fresh on every save, so a later re-reconcile
         * uses the latest choices, not the stale startup blob. `null` = loaded, none stored.
         */
        this._storedPreferences = null;
        /**
         * Whether the user has changed any preference this session. **Shared per session**
         * (was per-element, which let the two views disagree and one clobber the other's
         * choice — issue #1). Once set, the reconciler freezes so `chatConfig` defaults
         * can't overwrite a live user choice, regardless of which view toggled it.
         */
        this._preferencesUserChanged = false;
    }
    /**
     * Re-point the dependency getters at the currently-live element view. The
     * persister outlives any single element (it's cached per session in
     * `persister-registry.ts`), so when a later element mounts on the same session
     * — e.g. an SPA teardown + remount — it MUST rebind, or the persister keeps
     * calling the torn-down creator's `getDiagnosticEntries`/`getPersistence`/
     * `getAgentNames` (which read a detached driver/store → empty), silently halting
     * diagnostics + autosave capture (GENC-1351 §5.10). `stateKey` is unchanged.
     *
     * @internal
     */
    rebindDeps(deps) {
        this.deps = deps;
    }
    /** The loaded preferences blob (shared per session); read by the element's reconciler. */
    get storedPreferences() {
        return this._storedPreferences;
    }
    /** True once the user has changed any preference this session (shared per session). */
    get preferencesUserChanged() {
        return this._preferencesUserChanged;
    }
    get stateKey() {
        return this.deps.stateKey;
    }
    logMeta(type, detail) {
        recordMetaEvent(this.stateKey, type, detail);
    }
    /**
     * Throttled autosave. Called by every view on each transcript change; the single
     * shared timer coalesces them into at most one save per the provider's
     * `saveDebounceMs`. `saveDebounceMs: 0` (e.g. `sessionStorage`) means save as
     * soon as the current tick settles.
     */
    scheduleSave() {
        const { provider, enabled } = this.deps.getPersistence();
        if (!enabled || !provider)
            return;
        if (this._saveTimer != null)
            return; // a save is already pending — coalesce
        const elapsed = Date.now() - this._lastSaveAt;
        const delay = Math.max(0, provider.saveDebounceMs - elapsed);
        this._saveTimer = setTimeout(() => {
            this._saveTimer = undefined;
            this._lastSaveAt = Date.now();
            void this.persistSession();
        }, delay);
    }
    /**
     * Cancel a pending debounced autosave **without flushing it** (GENC-1351 §5.12).
     * Called before a session switch/re-key so the outgoing transcript can never be
     * written to the incoming session's file after the key changes — the clobber
     * guard for in-place project switching.
     */
    cancelPendingSave() {
        if (this._saveTimer != null) {
            clearTimeout(this._saveTimer);
            this._saveTimer = undefined;
        }
    }
    /**
     * Capture the current session to durable storage (GENC-1351 §5.1). Reads the
     * full transcript from the driver and the restorable scalars from the store, and
     * captures the active stateful agent's `serialize()` into `agentSnapshots`.
     */
    persistSession() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c, _d, _e, _f, _g;
            const { provider, enabled } = this.deps.getPersistence();
            if (!enabled || !provider)
                return;
            const key = this.stateKey;
            const ref = this.deps.getStore();
            const driver = this.deps.getDriver();
            if (!ref)
                return;
            const s = ref.store.aiAssistant;
            // Strip the display-only restore notices (restored / couldn't-restore) so they
            // never persist or accumulate across reload→save→reload cycles.
            const messages = ((_b = (_a = driver === null || driver === void 0 ? void 0 : driver.getRawHistory) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _b !== void 0 ? _b : s.messages).filter((m) => !isSessionNoticeMarker(m));
            if (!messages.length)
                return; // nothing worth persisting (e.g. a just-cleared session)
            // Serialize the live active agent from the DRIVER (the instance whose
            // `onActivate` ran), not via the host's `agents` — a host whose `agents` getter
            // mints fresh configs (the showcase) would otherwise hand us a never-activated
            // closure whose `serialize()` returns undefined.
            const liveSnapshots = {};
            const liveActive = (_c = driver === null || driver === void 0 ? void 0 : driver.getActiveAgent) === null || _c === void 0 ? void 0 : _c.call(driver);
            if (liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize) {
                try {
                    const snap = liveActive.serialize();
                    if (snap !== undefined)
                        liveSnapshots[liveActive.name] = snap;
                }
                catch (e) {
                    logger.error('Session save: agent serialize() failed:', e);
                }
            }
            // A post-restore autosave (triggered by the restore's own history change) runs
            // BEFORE the pinned stateful agent re-activates, so the live capture is empty
            // even though a flow is mid-stream. Writing `{}` would wipe the machine and
            // reset the flow on the next reload — so carry the flow-owner's last-known-good
            // image forward (GENC-1351 §6). Pure + unit-tested.
            const { snapshots: agentSnapshots, cache } = reconcileAgentSnapshots(liveSnapshots, s.flowOwnerAgentName, this._cache);
            this._cache = cache;
            // Non-resumable-workflow gate (GENC-1351 §5.9): consult the active agent (the
            // flow-owner during a locked flow, like `serialize` above). A thrown predicate
            // fails safe to persisting; no live active agent (e.g. the post-restore,
            // pre-reactivation save) leaves it resumable and persists normally.
            let resumable = true;
            if (liveActive === null || liveActive === void 0 ? void 0 : liveActive.resumable) {
                try {
                    resumable = yield liveActive.resumable({
                        agentName: liveActive.name,
                        sessionKey: key,
                        state: agentSnapshots[liveActive.name],
                        messages,
                        isFlowOwner: s.flowOwnerAgentName === liveActive.name,
                    });
                }
                catch (e) {
                    logger.error('Session save: resumable() threw — persisting (fail-safe):', e);
                    resumable = true;
                }
            }
            if (!resumable) {
                // FREEZE while the flow runs: stop advancing the stored snapshot so the
                // in-flight agent's work is never persisted and the *previous* conversation is
                // kept intact ("infinite debounce while running"). The freeze target is the
                // **currently-persisted** snapshot — the shared source of truth — NOT any
                // per-view memory. Stamp it once with an `interrupted` marker so restore shows
                // an assistant "start again" message; once stamped, subsequent saves no-op.
                const existing = yield provider.load(key).catch(() => null);
                if (existing === null || existing === void 0 ? void 0 : existing.interrupted)
                    return; // already frozen + stamped by some instance
                const { resumeMessage, resumePrompt } = resolveNonResumableNotice({
                    notResumableMessage: liveActive === null || liveActive === void 0 ? void 0 : liveActive.notResumableMessage,
                    flowActivationPrompt: s.flowActivationPrompt,
                    messages,
                    defaultMessage: DEFAULT_NOT_RESUMABLE_MESSAGE,
                });
                // Freeze at the persisted snapshot (the last resumable state). Fallback when
                // nothing is stored yet — a genuinely fresh session that goes non-resumable on
                // its first turn, OR a load that failed (a corrupt read, a transient network
                // error): freeze at the current transcript **through the last user message**
                // (flow-state stripped), so we keep the conversation + the prompt that started
                // the flow but drop the agent's in-flight turns. NEVER an empty snapshot —
                // losing the conversation is the one outcome we must avoid.
                const frozen = existing !== null && existing !== void 0 ? existing : createPersistedSession({
                    sessionKey: key,
                    host: window.location.host,
                    messages: keepThroughLastUserMessage(messages),
                    activeAgentName: (_d = s.activeAgent) === null || _d === void 0 ? void 0 : _d.name,
                    pinnedAgentName: s.pinnedAgentName,
                    flowOwnerAgentName: null,
                    sessionCostUsd: s.sessionUsage.costUsd,
                    contextTokens: s.contextTokens,
                    contextLimit: s.contextLimit,
                    activeModel: s.activeModel,
                    activeProviderName: s.activeProviderName,
                    agentSnapshots: {},
                });
                this.logMeta('persistence.suppressed', {
                    agent: (_e = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _e !== void 0 ? _e : null,
                    isFlowOwner: s.flowOwnerAgentName === (liveActive === null || liveActive === void 0 ? void 0 : liveActive.name),
                    frozeAtMessages: frozen.messages.length,
                    custom: !!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.notResumableMessage),
                });
                try {
                    yield provider.save(key, Object.assign(Object.assign({}, frozen), { interrupted: { message: resumeMessage, prompt: resumePrompt } }));
                    logger.debug('Flow non-resumable — froze snapshot + stamped interrupted marker', { key });
                }
                catch (e) {
                    logger.error('Session save (interrupted marker) failed:', e);
                }
                return;
            }
            // Resumable → normal save (this overwrites any prior `interrupted` marker, so a
            // later non-resumable episode stamps afresh).
            const snapshot = createPersistedSession({
                sessionKey: key,
                host: window.location.host,
                messages: [...messages],
                activeAgentName: (_f = s.activeAgent) === null || _f === void 0 ? void 0 : _f.name,
                pinnedAgentName: s.pinnedAgentName,
                flowOwnerAgentName: s.flowOwnerAgentName,
                sessionCostUsd: s.sessionUsage.costUsd,
                contextTokens: s.contextTokens,
                contextLimit: s.contextLimit,
                activeModel: s.activeModel,
                activeProviderName: s.activeProviderName,
                agentSnapshots,
            });
            // Observability (GENC-1351): record what each autosave captured for stateful
            // agents — `agents: []` here means no machine snapshot was taken (the bug to
            // watch for: the active agent's `serialize()` returned nothing).
            this.logMeta('persistence.saved', {
                messages: messages.length,
                agents: Object.keys(agentSnapshots),
                active: (_g = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _g !== void 0 ? _g : null,
                hasSerialize: !!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize),
                agentSnapshots,
            });
            try {
                yield provider.save(key, snapshot);
                logger.debug('Session saved', { key, messages: messages.length });
            }
            catch (e) {
                logger.error('Session save failed:', e);
            }
        });
    }
    /**
     * Page-reload restore (GENC-1351 §5.3). With a stored snapshot for this session,
     * hydrate the store and reseed the driver so the conversation continues. No-op
     * when disabled, absent, or version/host mismatched. An `interrupted` snapshot
     * (§5.9) restores the conversation but shows a "start again" assistant message
     * instead of rehydrating the mid-flight flow.
     */
    restore() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c, _d, _e;
            const { provider } = this.deps.getPersistence();
            if (!provider)
                return;
            const key = this.stateKey;
            const driver = this.deps.getDriver();
            // Bound a slow/hung provider so a stuck load can't brick startup. On timeout
            // (or a load rejection) `withTimeout` throws — its late result is discarded by
            // the race, so it can never clobber a now-live session (§6). Crucially, a throw
            // is distinct from a resolved `null`: a throw means "there may have been a
            // session but we couldn't fetch it" → surface a notice; `null` means "nothing
            // stored" → start fresh silently.
            let snapshot;
            try {
                snapshot = yield withTimeout(provider.load(key), RESTORE_TIMEOUT_MS);
            }
            catch (e) {
                logger.warn('Session restore: load failed or timed out — starting fresh', e);
                this.showRestoreFailedNotice();
                return;
            }
            if (!snapshot)
                return; // genuinely nothing stored → fresh session, no notice
            try {
                if (!isRestorableSession(snapshot, { host: window.location.host })) {
                    // A saved snapshot existed but can't be used (schema bump / cross-host) —
                    // tell the user rather than silently dropping their conversation.
                    logger.debug('Session restore skipped: version/host mismatch', { key });
                    this.showRestoreFailedNotice();
                    return;
                }
                // Trim a trailing incomplete turn (orphaned tool call / placeholder / pending
                // interaction) so a snapshot saved mid-loop restores to the last
                // provider-valid boundary (§5.3 step 2).
                const trimmed = trimToRestorableBoundary(snapshot.messages).filter((m) => !isSessionNoticeMarker(m));
                const ref = this.deps.getStore();
                if (!ref)
                    return;
                // Diagnostics watermark (§5.8): messages present at restore already live in a
                // prior load's diagnostics stream — record their identities in the shared
                // per-session cursors so this load's forward capture never re-appends them
                // (only genuinely new activity streams). Shared, so the element view that
                // never runs restore honours the same watermark.
                getDiagnosticsCursors(key).restoredMsgKeys = new Set(flattenSubAgentMessages(trimmed)
                    .map((e) => diagnosticsMessageKey(e))
                    .filter((k) => k !== null));
                // Prime each stateful agent's persisted machine/state image FIRST — before
                // seeding Redux or driver history — so the image is waiting on the driver no
                // matter when the agent re-activates. The flow-owner lock self-heals on
                // activation (OrchestratingDriver.applyAgent re-sets it).
                (_a = driver === null || driver === void 0 ? void 0 : driver.primeRestoredAgentState) === null || _a === void 0 ? void 0 : _a.call(driver, snapshot.agentSnapshots);
                // Seed the carry-forward cache so the immediate post-restore autosave (fired
                // by the history change below, before any agent re-activates) preserves the
                // machine snapshot instead of overwriting it with an empty one (GENC-1351).
                this._cache = Object.assign({}, snapshot.agentSnapshots);
                // Observability (GENC-1351): record exactly what the restore primed, so a
                // post-reload debug-log export shows whether a stateful agent's snapshot
                // actually came back (meta events reset on reload, so this is the only window
                // onto the saved→loaded handoff).
                this.logMeta('persistence.restored', {
                    agents: Object.keys((_b = snapshot.agentSnapshots) !== null && _b !== void 0 ? _b : {}),
                    agentSnapshots: snapshot.agentSnapshots,
                    pinnedAgentName: snapshot.pinnedAgentName,
                    flowOwnerAgentName: snapshot.flowOwnerAgentName,
                });
                // Loudly surface saved agent state that can't be rehydrated because the agent
                // isn't in the current set — otherwise the flow silently fails to resume, a
                // real footgun for hosts with dynamic agent arrays. Non-fatal: the transcript
                // and any known agents still restore.
                const orphanedAgents = unknownAgentSnapshots(snapshot, this.deps.getAgentNames());
                if (orphanedAgents.length) {
                    logger.error(`Session restore: saved state for agent(s) [${orphanedAgents.join(', ')}] cannot be ` +
                        `rehydrated — not in the configured agents, so their flow will not resume. ` +
                        `(The transcript and any known agents still restore.)`, { key });
                    this.logMeta('persistence.restore-orphan', {
                        orphanedAgents,
                        knownAgents: this.deps.getAgentNames(),
                    });
                }
                // Trailing rows appended to the restored transcript:
                //  • interrupted flow (§5.9) → the user's triggering message (when captured) followed
                //    by a real `assistant` "start again" message. Both are normal chat bubbles,
                //    persisted as part of the conversation, so the exchange reads naturally — the user
                //    sees what they asked and the assistant's reply explaining it didn't complete
                //    (instead of an assistant reply to an invisible message). The frozen snapshot never
                //    rehydrates the in-flight flow (its `agentSnapshots` are the last *resumable* state),
                //    so the triggering message is text only — none of the dropped work comes back.
                //  • otherwise → the subtle "restored" `system-event` divider (§10-O2),
                //    display-only and stripped again on the next save.
                const trailerTs = new Date().toISOString();
                let trailers;
                if (snapshot.interrupted) {
                    trailers = [];
                    // Re-append the triggering user message UNLESS the frozen transcript already
                    // ends with it. The two freeze paths differ: an *existing* prior snapshot is
                    // older than the trigger (so the trigger is absent and must be re-added), but
                    // the *fresh-freeze fallback* keeps the transcript through the last user
                    // message (`keepThroughLastUserMessage`), so the trigger is already present —
                    // re-appending it there would duplicate the bubble (GENC-1351 §5.9).
                    const lastTrimmed = trimmed[trimmed.length - 1];
                    const promptAlreadyPresent = (lastTrimmed === null || lastTrimmed === void 0 ? void 0 : lastTrimmed.role) === 'user' && lastTrimmed.content === snapshot.interrupted.prompt;
                    if (snapshot.interrupted.prompt && !promptAlreadyPresent) {
                        trailers.push({
                            role: 'user',
                            content: snapshot.interrupted.prompt,
                            timestamp: trailerTs,
                        });
                    }
                    trailers.push({
                        role: 'assistant',
                        content: snapshot.interrupted.message,
                        timestamp: trailerTs,
                    });
                }
                else if (trimmed.length === 0) {
                    trailers = [];
                }
                else {
                    trailers = [
                        {
                            role: 'system-event',
                            content: SESSION_RESTORED_LABEL,
                            timestamp: trailerTs,
                        },
                    ];
                }
                const messages = [...trimmed, ...trailers];
                ref.actions.aiAssistant.loadSession(toLoadSessionPayload(Object.assign(Object.assign({}, snapshot), { messages })));
                (_c = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _c === void 0 ? void 0 : _c.call(driver, [...messages]);
                // Interrupted flow (§5.9): pre-fill the composer with the prompt that started
                // the flow so "send it again" is one keystroke, and record it for observability.
                if (snapshot.interrupted) {
                    if (snapshot.interrupted.prompt) {
                        ref.actions.aiAssistant.setInputValue(snapshot.interrupted.prompt);
                    }
                    this.logMeta('persistence.not-resumable', {
                        hadPrompt: !!snapshot.interrupted.prompt,
                        messages: trimmed.length,
                    });
                }
                // Propagate the restored pin AND flow-owner lock to the DRIVER's routing (GENC-1351).
                // `loadSession` only hydrates the store; the driver learns them via `setPinnedAgent`/
                // `setFlowOwner`, which otherwise ran at `wireDriver` time — before restore, when the
                // store values were still null. Without this the first post-restore message falls
                // through to the classifier and re-routes away from the flow agent. The pin covers a
                // user-pinned agent; the flow-owner lock covers a *classifier-routed* stateful flow
                // (flow-owned but unpinned) — `resolvePinnedAgent` reads `flowOwnerAgentName ??
                // pinnedAgentName`, so restoring both resumes the flow without re-classifying, and the
                // driver's routing agrees with the restored (picker-locked) store state.
                if (driver instanceof OrchestratingDriver) {
                    driver.setPinnedAgent((_d = snapshot.pinnedAgentName) !== null && _d !== void 0 ? _d : null);
                    driver.setFlowOwner((_e = snapshot.flowOwnerAgentName) !== null && _e !== void 0 ? _e : null);
                }
                logger.debug('Session restored', { key, messages: messages.length });
            }
            catch (e) {
                logger.error('Session restore failed:', e);
                this.showRestoreFailedNotice();
            }
        });
    }
    /**
     * Surface a non-fatal "couldn't restore" notice after a failed restore (load
     * timeout/error, or an unusable snapshot) so a vanished conversation isn't a
     * silent mystery on reload (GENC-1351 §6). Reuses the display-only `system-event`
     * divider (like the "Restored…" one) — stripped before persist, so it never lands
     * in the fresh session's saved history. `loadHistory` is safe here: this path only
     * runs when nothing was restored, so the driver is empty.
     */
    showRestoreFailedNotice() {
        var _a, _b;
        (_b = (_a = this.deps.getDriver()) === null || _a === void 0 ? void 0 : _a.loadHistory) === null || _b === void 0 ? void 0 : _b.call(_a, [
            {
                role: 'system-event',
                content: SESSION_RESTORE_FAILED_LABEL,
                timestamp: new Date().toISOString(),
            },
        ]);
    }
    /**
     * Drop the restore-core snapshot (the "remember chat" toggle-off and the Clear
     * action) and reset the carry-forward cache. Diagnostics + preferences are
     * separate and untouched here.
     */
    clearRestoreCore() {
        return __awaiter(this, void 0, void 0, function* () {
            this._cache = {};
            const { provider } = this.deps.getPersistence();
            const key = this.stateKey;
            if (!provider)
                return;
            try {
                yield provider.clear(key);
            }
            catch (e) {
                logger.error('Session clear failed:', e);
            }
        });
    }
    // ── Preferences (GENC-1351 §5.10 Phase 2) ──────────────────────────────────
    // The persister owns the provider I/O for the stored preferences blob; the
    // element keeps `gatherPreferences` + the `chatConfig`-defaults reconciler
    // (`applyPreferenceBaseline`), which needs `this.chatConfig`.
    /**
     * Load the remembered preferences blob (bounded by {@link RESTORE_TIMEOUT_MS}) into
     * {@link storedPreferences}. A throw propagates to the caller (the element wraps it
     * and falls back to config defaults); on success `null` = no provider / none stored.
     */
    loadPreferences() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const { provider } = this.deps.getPersistence();
            if (!provider) {
                this._storedPreferences = null;
                return;
            }
            const prefs = yield withTimeout(Promise.resolve((_a = provider.loadPreferences) === null || _a === void 0 ? void 0 : _a.call(provider, this.stateKey)), RESTORE_TIMEOUT_MS);
            this._storedPreferences = prefs !== null && prefs !== void 0 ? prefs : null;
        });
    }
    /**
     * Persist the preferences blob. Only ever called on a genuine user change, so it
     * also records that (freezing the reconciler — shared per session) and keeps
     * {@link storedPreferences} fresh. Fires **regardless of "remember chat"** — display
     * choices are the user's — needing only a provider that implements `savePreferences`.
     */
    savePreferences(prefs) {
        this._preferencesUserChanged = true;
        this._storedPreferences = prefs;
        const { provider } = this.deps.getPersistence();
        if (!(provider === null || provider === void 0 ? void 0 : provider.savePreferences))
            return;
        void provider
            .savePreferences(this.stateKey, prefs)
            .catch((e) => logger.error('Persistence: savePreferences failed:', e));
    }
    // ── Diagnostics append (GENC-1351 §5.10 Phase 3) ───────────────────────────
    // One append scheduler per session (was per-view). Entry *building* stays
    // element-owned (`getDiagnosticEntries`); the cursor-diffing + append live here.
    /**
     * Throttled diagnostics append (GENC-1351 §5.8). On each transcript change, if the
     * provider persists diagnostics, coalesce and append the new activity since the
     * last flush (coarser than the restore-core save — the log is heavy). Keyed on
     * provider capability only, **independent of "remember chat"**. No-op without a
     * diagnostics-capable provider, so it's free for other consumers.
     */
    scheduleDiagnostics() {
        var _a, _b;
        const { provider } = this.deps.getPersistence();
        if (!(provider === null || provider === void 0 ? void 0 : provider.appendDiagnostics))
            return;
        // Don't capture mid-restore: the watermark (restoredMsgKeys) is set during
        // restore, and a flush before it lands would re-append the restored history.
        // `restoring` is slice-backed (shared), so every view holds off (§5.8/§6).
        if ((_a = this.deps.getStore()) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.restoring)
            return;
        if (this._diagnosticsTimer != null)
            return; // coalesce
        const debounce = (_b = provider.diagnosticsDebounceMs) !== null && _b !== void 0 ? _b : DEFAULT_DIAGNOSTICS_DEBOUNCE_MS;
        const delay = Math.max(0, debounce - (Date.now() - this._lastDiagnosticsAt));
        this._diagnosticsTimer = setTimeout(() => {
            this._diagnosticsTimer = undefined;
            this._lastDiagnosticsAt = Date.now();
            void this.flushDiagnostics();
        }, delay);
    }
    /**
     * Append the diagnostic entries created since the last flush to the provider's
     * forward-only stream. Best-effort — a failed write is logged and never touches
     * the chat. (The cursors advance on collect, so a dropped batch isn't retried;
     * acceptable for a forensic stream that isn't the source of truth.) Also called
     * directly by the download path to land the current delta before reassembly.
     */
    flushDiagnostics() {
        return __awaiter(this, void 0, void 0, function* () {
            const { provider } = this.deps.getPersistence();
            if (!(provider === null || provider === void 0 ? void 0 : provider.appendDiagnostics))
                return;
            const delta = this.collectDiagnosticsDelta();
            if (!delta.length)
                return;
            try {
                yield provider.appendDiagnostics(this.stateKey, delta);
            }
            catch (e) {
                logger.error('Diagnostics append failed:', e);
            }
        });
    }
    /**
     * The diagnostic entries not yet appended this load — the forward-capture delta
     * (§5.8). Skips messages present at restore or already appended; emits each turn
     * (by turn-index) and meta-event (by index) once; appends a `meta-snapshot` only
     * when the block changed. Advances the shared per-session cursors as it goes.
     *
     * TODO(GENC-1461, longer-term): this whole delta step exists only because the element *pulls* the
     * ENTIRE debug log on every throttled flush (`getDiagnosticEntries()` returns the full timeline),
     * so we must diff it against per-session cursors to append only the new tail — and messages, unlike
     * events (monotonic `index`) and turns (`turnIndex`), have no stable positional cursor (compaction
     * rewrites ranges, tool-call messages are annotated in place, synthetic messages are spliced in,
     * restore reloads them), forcing a content-derived identity (`diagnosticsMessageKey`). That
     * identity is inherently fragile: it was too coarse for the reasoning/answer/tool-call split and
     * silently dropped co-timestamped messages (GENC-1461; fixed for now by adding `category`). The
     * robust fix is to invert the flow — have the driver *push* each diagnostic entry exactly once at
     * creation (an append-only event stream) instead of the element pulling-and-diffing — which removes
     * the message-identity key and this dedup entirely. Bigger change; deferred.
     */
    collectDiagnosticsDelta() {
        var _a;
        const cursors = getDiagnosticsCursors(this.stateKey);
        const delta = [];
        for (const entry of this.deps.getDiagnosticEntries()) {
            if (entry.kind === 'message') {
                const key = diagnosticsMessageKey(entry);
                if (key !== null) {
                    if (cursors.restoredMsgKeys.has(key) || cursors.emittedMsgKeys.has(key))
                        continue;
                    cursors.emittedMsgKeys.add(key);
                }
                delta.push(entry);
            }
            else if (entry.kind === 'turn') {
                const key = String((_a = entry.turnIndex) !== null && _a !== void 0 ? _a : '');
                if (cursors.emittedTurnKeys.has(key))
                    continue;
                cursors.emittedTurnKeys.add(key);
                delta.push(entry);
            }
            else if (entry.kind === 'event') {
                const index = typeof entry.index === 'number' ? entry.index : -1;
                if (index <= cursors.lastEventIndex)
                    continue;
                cursors.lastEventIndex = index;
                delta.push(entry);
            }
            else {
                // meta-snapshot — append only when the near-static config actually changed.
                // Prefer the entry's `dedupSignature` (the config-only projection the element
                // builds), so per-flush churn in the volatile fields (export timestamp,
                // context tokens/cost, live debug snapshot) no longer forces a re-append of
                // the bulky block every time (GENC-1351 §5.8 / #12). Falls back to the full
                // `meta` for any entry that predates the signature.
                const sig = typeof entry.dedupSignature === 'string'
                    ? entry.dedupSignature
                    : JSON.stringify(entry.meta);
                if (sig === cursors.lastMetaJson)
                    continue;
                cursors.lastMetaJson = sig;
                delta.push(entry);
            }
        }
        return delta;
    }
    /** Cancel any pending timers. Called on explicit session teardown. */
    dispose() {
        clearTimeout(this._saveTimer);
        this._saveTimer = undefined;
        clearTimeout(this._diagnosticsTimer);
        this._diagnosticsTimer = undefined;
    }
}
