/**
 * TopicProfileOrchestrator — the §8 orchestration core of TOPIC-PROFILE-SPEC:
 * the debounce / idle-confirmation / kill-respawn machinery that applies a
 * profile change SAFELY, plus the §10.4 spawn-failure circuit breaker and the
 * §14 dry-run shadow regime.
 *
 * Spec clauses owned here (each enforced in code, not prose):
 *
 *  §8 two-phase lock — the per-topic store lock is held in TWO SHORT PHASES,
 *    never across the debounce window: WRITE (mutate → arm/extend debounce →
 *    release) and RESPAWN (re-acquire → re-resolve AT THIS MOMENT → re-confirm
 *    idle → [kill → respawn] → release). Re-resolution + idle re-confirm +
 *    net-unchanged skip run at DEQUEUE time inside the lock (round-4).
 *
 *  Debounce slots — per-topic pending slot + trailing-edge timer; the store
 *    write is immediate, only the RESPAWN is debounced. N changes in the
 *    window collapse to ONE respawn against the final resolved profile; a
 *    net-unchanged sequence fires ZERO respawns and closes its loop out loud.
 *    The framework-switch arm carries the heavier window.
 *
 *  Idle is a precondition re-checked at kill time, never a value carried from
 *    classification (round-2 TOCTOU). The read is three-valued; at kill time
 *    UNCONFIRMED IS BUSY (defer) — never permission to kill. Pane-idle is not
 *    task-done: the autonomous-session registry is consulted, and an active
 *    autonomous/time-boxed run is busy until it completes (only the explicit
 *    "switch now" confirm overrides). PROTECTED SESSIONS ARE NEVER
 *    PROFILE-KILLED — and "switch now" never overrides protection (round-5).
 *
 *  Kill-path precision — a same-framework resume respawn kills via the
 *    resume-saving path (the re-save is wanted); a FRESH respawn PARKS (never
 *    deletes) BOTH resume stores' entries before the kill and sets a
 *    topic-scoped, time-bounded durable suppression marker so no writer
 *    (heartbeat, post-spawn save, kill listener, shutdown save) can re-persist
 *    a stale id during the window — the gates are installed at the resume-map
 *    chokepoints via claudeResumeWriteGate()/codexResumeWriteGate().
 *
 *  Disclosure-of-record — EVERY accepted write discloses; writes inside the
 *    active debounce window coalesce into ONE delta-carrying disclosure
 *    ("was: <pre-burst> → now: <final> (N changes)"); a per-topic rate cap
 *    backstops outside the window with a delta-carrying overflow summary; the
 *    undo snapshot (store.mutate shiftPrevious) moves once per disclosed
 *    burst — undo always restores the profile the operator last saw disclosed
 *    (R7-4). Disclosures carry the audit seq and set the relay dedup bypass.
 *
 *  Global stagger — profile-triggered respawns share a global concurrency cap
 *    (max K in flight, FIFO queue); same-cwd codex spawns are serialized so
 *    two codex sessions never spawn inside the same capture fence window
 *    (bounded wait: prior fence resolves or the RESPAWN-phase TTL, round-7).
 *
 *  §9 interplay — only operator pin writes arm the respawn debounce;
 *    escalation consults read-only and serializes through runExclusive().
 *    Any profile-triggered kill clears the topic's escalation marker and
 *    releases its lease BEFORE computing expected-live; expected-live =
 *    resolved baseline ⊕ any active escalation marker (no controller
 *    ping-pong).
 *
 *  §10.4 breaker — LIVE in every regime (exempt from BOTH `enabled` and
 *    `dryRun`; legacy-path failures count when attributable). Attribution is
 *    an ALLOWLIST (cli-not-found / launch-arg-rejected /
 *    model-rejected-by-account); ambient classes never increment; the counter
 *    resets on any successful spawn. A trip parks the profile
 *    intended-but-unhealthy, reverts to last-known-good (or default),
 *    un-parks the matching-framework resume entry (none-loss when the
 *    transcript survives), notifies the operator, audits
 *    system:circuit-breaker, and respawns IMMEDIATELY (the one keep-working
 *    exception to regime gating). Re-apply of the same recently-tripped
 *    profile requires an explicit cooldown confirm; switch-now,
 *    propose-confirm and the cooldown confirm share ONE armed slot per topic.
 *
 *  §14 dry-run shadow — under `enabled && dryRun`, NEW-axis writes persist to
 *    the shadow intendedProfile field that resolution ignores ([dry-run]
 *    notice); the true→false flip CLEARS every shadow (never promotes) with
 *    one coalesced expired-intents notice; exempted framework writes and the
 *    recovery writes (re-apply / clear) are always LIVE store writes in every
 *    regime, with the recovery writes' APPLICATION arm regime-governed (no
 *    profile-triggered kill outside fully-live; told out loud).
 *
 * Dependency-injected throughout (ModelSwapService house style): every
 * side-effecting surface is a constructor dep; pure logic
 * (classifyProfileChange, profilesEqual) is imported directly.
 */
import { type IdleReading, type SwapMethod } from './classifyProfileChange.js';
import { type TopicProfileStore } from './TopicProfileStore.js';
import type { ResolvedTopicProfile } from './TopicProfileResolver.js';
import type { EffortLevel, ProfileModelTier, ThinkingMode, ValidatedProfilePatch } from './topicProfileValidation.js';
import type { CodexSpawnFence, FenceCaptureResult } from './CodexResumeMap.js';
import type { IntelligenceFramework } from './intelligenceProviderFactory.js';
export type ProfileWriteOrigin = 'conversational' | 'slash' | 'http' | 'transfer' | 'system';
/**
 * §10.4 failure classes. ONLY the allowlist counts toward the breaker —
 * ambient classes (conditions that would fail ANY profile) never increment,
 * and resume-id-mismatch is the resume map's failure, not the profile's.
 */
export type ProfileSpawnFailureClass = 'cli-not-found' | 'launch-arg-rejected' | 'model-rejected-by-account' | 'resume-id-mismatch' | 'quota' | 'tmux' | 'disk' | 'unknown';
/** The launch characteristics actually applied to a live session at spawn. */
export interface AppliedProfile {
    framework: IntelligenceFramework;
    /** Concrete model id, or null for the account default. */
    model: string | null;
    /** Tier shape of the pin that produced the model, when tier-shaped. */
    modelTier: ProfileModelTier | null;
    thinkingMode: ThinkingMode | null;
    /** Claude `--effort` level applied at spawn, or null. */
    effort: EffortLevel | null;
}
export interface OrchTopicSession {
    sessionName: string;
    cwd: string;
}
export interface RespawnSpawnOutcome {
    ok: boolean;
    failureClass?: ProfileSpawnFailureClass;
}
/** The narrow session surface the orchestrator is allowed to touch. */
export interface OrchestratorSessionPort {
    getSessionForTopic(topicKey: string): OrchTopicSession | null;
    /** All live topic-bound sessions (boot reconcile sweep). */
    listTopicSessions(): Array<{
        topicKey: string;
        sessionName: string;
    }>;
    /** FABLE capture-pane idle confirmation — three-valued (§8). */
    readIdle(sessionName: string): IdleReading;
    /**
     * Kill via SessionManager.killSession — fires beforeSessionKill, so the
     * resume re-save happens (WANTED for a same-framework resume respawn).
     */
    killForResume(sessionName: string): Promise<boolean>;
    /**
     * Fresh-respawn kill: direct tmux path or suppression-set kill — NO resume
     * re-save may fire (the §8 symmetric-poisoning rule; the durable
     * suppression marker backstops writers that fire anyway).
     */
    killFresh(sessionName: string): Promise<boolean>;
    /** Spawn the topic's session with the resolved profile. */
    spawn(topicKey: string, resolved: ResolvedTopicProfile, directive: {
        method: SwapMethod;
        resumeId?: string;
    }): Promise<RespawnSpawnOutcome>;
}
export interface ClaudeResumePort {
    /** §8 pre-kill predicate: hook-provenance resume readiness. */
    ready(topicKey: string): boolean;
    resumeId(topicKey: string): string | null;
    park(topicKey: string, reason: string): void;
    unpark(topicKey: string): boolean;
}
export interface CodexResumePort {
    get(topicKey: string): string | null;
    captureAtKill(topicKey: string, sessionName: string, fence: CodexSpawnFence): Promise<FenceCaptureResult>;
    park(topicKey: string, reason: string): void;
    unpark(topicKey: string): boolean;
}
/** §9 — FABLE's ephemeral last-applied-tier marker, read-only + clear. */
export interface EscalationPort {
    /** The active escalation marker for a topic (escalated model id), or null. */
    activeMarker(topicKey: string): {
        model: string;
    } | null;
    /** All topics holding markers (boot-sweep stale-marker clear). */
    listMarkerTopics(): string[];
    /** Profile-triggered kill / stale sweep: clear marker + release the lease. */
    clearMarkerAndReleaseLease(topicKey: string): void;
}
/** The §7 in-flight row — the FABLE model-swap route (tier only, never an id). */
export interface InFlightSwapPort {
    swap(sessionName: string, tier: 'default' | 'escalated'): Promise<{
        status: 'swapped' | 'unconfirmed' | 'dry-run' | 'noop' | 'refused';
        reason?: string;
    }>;
}
/** §6/§14 verification markers feeding the classifier's contingency flags. */
export interface ProfileVerificationFlags {
    inFlightSwapConfirmedRecently: boolean;
    thinkingOffOnResumeVerified: boolean;
    thinkingLevelResumeVerified: boolean;
    crossModelResumeVerified: boolean;
    claudeThinkingControlAvailable: boolean;
}
export interface OrchestratorConfig {
    /** RESOLVED gate value (DEV_GATED_FEATURES / operator override). */
    enabled: boolean;
    dryRun: boolean;
    respawnDebounceMs: number;
    frameworkSwitchDebounceMs: number;
    maxConcurrentProfileRespawns: number;
    spawnFailureBreakerThreshold: number;
    switchNowConfirmTtlMs: number;
}
export interface DisclosureMeta {
    /** Relay exact-duplicate suppression bypass (§8 — disclosure-of-record). */
    allowDuplicate: true;
    auditSeq: number;
}
export interface TopicProfileOrchestratorDeps {
    store: TopicProfileStore;
    /** The §5.2 single resolution point (in-memory, O(1)). */
    resolveProfile: (topicKey: string) => ResolvedTopicProfile;
    sessions: OrchestratorSessionPort;
    claudeResume: ClaudeResumePort;
    codexResume: CodexResumePort;
    escalation: EscalationPort;
    inFlightSwap: InFlightSwapPort;
    /** §8 — an active autonomous/time-boxed session is busy until it completes. */
    autonomousActive: (topicKey: string) => boolean;
    isProtectedSession: (sessionName: string) => boolean;
    /** The codex spawn fence recorded at launch for a topic (§7), or null. */
    codexFence: (topicKey: string) => CodexSpawnFence | null;
    verification: () => ProfileVerificationFlags;
    getConfig: () => OrchestratorConfig;
    /** Platform-routed disclosure to the topic's owning conversation (§10.5). */
    disclose: (topicKey: string, text: string, meta: DisclosureMeta) => void;
    /** Structured audit sink → logs/topic-profile-changes.jsonl (§10.3). */
    audit: (event: Record<string, unknown>) => void;
    /** Durable orchestrator side-state (lastApplied / suppressions / trips). */
    stateFilePath: string;
    now?: () => number;
}
export interface ProfileWriteAttribution {
    /** Server-verified principal (or 'api-token' / 'system:*') — never body-supplied. */
    updatedBy: string;
    origin: ProfileWriteOrigin;
}
export type ProfileWriteOutcome = {
    outcome: 'applied';
    reply: string;
    auditSeq: number;
    supersededParked: boolean;
} | {
    outcome: 'shadow-recorded';
    reply: string;
    auditSeq: number;
} | {
    outcome: 'no-change';
    reply: string;
} | {
    outcome: 'refused';
    reply: string;
    reason: string;
};
export type RecoveryWriteOutcome = {
    outcome: 'applied';
    reply: string;
} | {
    outcome: 'confirm-required';
    reply: string;
} | {
    outcome: 'refused';
    reply: string;
    reason: string;
};
type ConfirmKind = 'switch-now' | 'cooldown-reapply' | 'propose';
export declare class TopicProfileOrchestrator {
    private readonly deps;
    private readonly now;
    private readonly pendingSlots;
    private readonly confirmSlots;
    private readonly confirmArmTimes;
    private confirmArmCooldownUntil;
    /** FIFO respawn queue + global stagger cap (§8). */
    private readonly respawnQueue;
    private inFlightRespawns;
    private readonly activeRespawnTopics;
    private drainWakeTimer;
    /** Same-cwd codex fence windows (cwd → until epoch ms). */
    private readonly codexFenceWindows;
    /** Per-topic immediate-disclosure timestamps + overflow accumulation. */
    private readonly disclosureTimes;
    private readonly overflow;
    private durable;
    private auditSeq;
    private lastSeenDryRun;
    constructor(deps: TopicProfileOrchestratorDeps);
    /**
     * Apply a validated NEW-axis profile patch under the current regime.
     *  - fully-live (`enabled && !dryRun`): live mutate + debounce-armed respawn.
     *  - `enabled && dryRun`: shadow intendedProfile write, `[dry-run]` notice —
     *    resolution never reads it, the flip never promotes it (§14).
     *  - `!enabled`: REFUSED (existing pins stay honored on read) — EXCEPT a
     *    clear-only patch, which is a §5.2(b) recovery write, live everywhere.
     * The caller has already run §10.1 identity + §10.2 validation; this method
     * re-enforces the store-side invariants regardless (defense in depth).
     */
    requestProfileChange(topicKey: number | string, patch: ValidatedProfilePatch, attribution: ProfileWriteAttribution): Promise<ProfileWriteOutcome>;
    /**
     * §5.2(d) — an exempted framework-arm write (the legacy `/route` lane):
     * ALWAYS a live store write regardless of `enabled`/`dryRun`. The legacy
     * path performs its own immediate respawn; NO §8 machinery is engaged here
     * — but under `enabled && dryRun` the §8 orchestration runs in SHADOW,
     * logging the `[dry-run]` decisions it WOULD have made (audit-only, never
     * operator-facing). Returns the disclosure-of-record metadata the legacy
     * reply must carry (audit stamp + dedup bypass + parked-supersession name).
     */
    applyExemptFrameworkWrite(topicKey: number | string, framework: IntelligenceFramework, attribution: ProfileWriteAttribution): Promise<{
        changed: boolean;
        auditSeq: number;
        supersededParked: boolean;
        /** Append to the legacy reply when non-null (R10-3/R13). */
        supersessionNote: string | null;
        /** Stamp + dedup-bypass the legacy reply with these (R13). */
        meta: DisclosureMeta;
    }>;
    /**
     * §5.2(b) recovery writes — re-apply a §10.4 parked pin, or clear. LIVE in
     * every regime (never refused as a new pin, never shadowed). The
     * APPLICATION arm is regime-governed: outside fully-live there is NO
     * profile-triggered kill — the write applies at the next natural spawn /
     * boot sweep, and the confirmation says so out loud.
     */
    requestRecoveryWrite(topicKey: number | string, action: 'reapply' | 'clear', attribution: ProfileWriteAttribution, opts?: {
        confirmed?: boolean;
    }): Promise<RecoveryWriteOutcome>;
    private applyLiveWrite;
    /**
     * §8 write-surface seam (ProfileOrchestratorLike). Called by the write
     * surface AFTER it has performed the live store write in the fully-live
     * regime — the surface owns the mutate, the orchestrator owns the debounced,
     * idle-gated respawn. Re-resolves nothing here: it arms the per-topic
     * debounce window (heavier window when the framework axis changed) and the
     * trailing-edge fire enqueues the §8 respawn. A no-op when the orchestrator
     * is not in the fully-live regime (the surface only calls this when fully-live,
     * but the guard keeps the seam safe under a regime flip mid-burst).
     */
    onProfileWrite(topicKey: number | string, info: {
        frameworkChanged: boolean;
        origin: string;
    }): void;
    private armRespawn;
    private onDebounceFire;
    private enqueueRespawn;
    /** Global stagger: max K respawns in flight; FIFO; codex same-cwd serialized. */
    private drainQueue;
    private codexBlockedUntil;
    private scheduleDrainWake;
    /**
     * RESPAWN phase — re-acquire the per-topic lock, re-resolve AT THIS MOMENT,
     * re-confirm idle, then [kill → respawn]. TTL-bounded: a wedged kill/spawn
     * aborts the phase and leaves the divergence to the reconcile sweep.
     */
    private executeRespawn;
    private respawnPhase;
    private deferSlot;
    private teardownSlot;
    /** Terminal disclosure: coalesced burst delta + the honest swap line. */
    private discloseTerminal;
    /**
     * Arm a confirm. All three confirm surfaces (switch-now, propose-confirm,
     * cooldown-reapply) share THIS one slot per topic: arming supersedes the
     * prior armed confirm (the §10.1(c) re-echo discipline — a bare "yes" can
     * only ever fire the most-recently-echoed confirm). Re-proposals are
     * rate-bounded; churn past the bound tears the slot down for a cooldown
     * and is audited as a suspicion signal.
     */
    armConfirm(topicKey: number | string, kind: ConfirmKind, echo: string, run: () => Promise<string>): boolean;
    /** Record the platform message id of the rendered echo (§10.1 ordering). */
    attachConfirmEchoMessageId(topicKey: number | string, messageId: number): void;
    /**
     * Fire the armed confirm. The caller (server-side ingress parse) has
     * already established first-party origin + forward-metadata exclusion;
     * this enforces TTL, supersession, and the platform-message-id ordering
     * when ids are available (a confirm answering a superseded/older echo is
     * refused toward re-echo).
     */
    fireConfirm(topicKey: number | string, opts?: {
        messageId?: number;
    }): Promise<{
        fired: boolean;
        reply: string;
    }>;
    /** The currently-armed confirm echo (readout / re-echo surfaces). */
    armedConfirm(topicKey: number | string): {
        kind: ConfirmKind;
        echo: string;
    } | null;
    /**
     * Operator's "switch now" — overrides busy/autonomous deferral for the
     * SPECIFIC pending change that armed it. NEVER overrides protection.
     * With no armed pending switch this is a no-op with a plain reply.
     */
    handleSwitchNow(topicKey: number | string): Promise<{
        fired: boolean;
        reply: string;
    }>;
    private executeSwitchNow;
    /**
     * Called at EVERY successful spawn (natural, sentinel, or profile-triggered;
     * wiring obligation on the spawn path): records the last-applied-profile
     * marker (durable — the boot sweep compares against it across restarts),
     * resets the breaker counter, clears any suppression, and registers the
     * codex fence window for same-cwd serialization.
     */
    recordSpawnSuccess(topicKey: number | string, applied: AppliedProfile, opts?: {
        cwd?: string;
    }): void;
    /**
     * Called on a failed spawn attempt for a topic, from ANY path — including
     * the legacy `/route` respawn (§10.4: legacy-path failures count when
     * attributable; the breaker is never dormant).
     */
    recordSpawnFailure(topicKey: number | string, failureClass: ProfileSpawnFailureClass): void;
    private recordSpawnFailureInternal;
    /**
     * §10.4 trip: park the failing profile intended-but-unhealthy, revert to
     * last-known-good (or the global default), un-park the matching-framework
     * resume entry (the revert is none-loss when the transcript survives),
     * notify, audit system:circuit-breaker, and respawn IMMEDIATELY — the one
     * keep-working exception, live in EVERY regime.
     */
    private tripBreaker;
    /** Marks a topic's codex capture fence resolved (rollout observed). */
    markCodexFenceResolved(cwd: string): void;
    /**
     * Piggybacks an existing periodic cadence (reaper/watchdog — no per-topic
     * pollers): retries deferred (busy-abort) swaps and re-checks the dry-run
     * flip lever.
     */
    tick(): void;
    /**
     * Boot-time reconcile sweep (§8 round-3): the pending slot is
     * process-local, but tmux sessions and the store survive a restart
     * mid-debounce. Compares each live topic-bound session's last-applied
     * profile against the store (⊕ escalation marker) and arms the normal
     * debounced, idle-gated respawn on divergence — in the fully-live regime.
     * In gated regimes divergence is audited and left to the next natural
     * spawn (no profile-triggered kill outside fully-live, §5.2(b)).
     * Stale escalation markers (session gone) are cleared FIRST.
     */
    bootReconcileSweep(): void;
    /**
     * §14 — the dryRun true→false flip clears EVERY topic's shadow (intents
     * are NEVER promoted, at the flip or ever) and surfaces the expired
     * would-be intents ONCE as a single coalesced notice.
     */
    checkDryRunFlip(): void;
    /**
     * Lock pass-through for the escalation authority (§5.1/§9): an escalation
     * swap serializes its live-session mutation through the SAME per-topic
     * lock — ordering, not shared fields (it writes only its own marker and
     * NEVER arms the respawn debounce).
     */
    runExclusive<T>(topicKey: number | string, fn: () => Promise<T> | T): Promise<T>;
    /**
     * §8 resume-writer gates — install via TopicResumeMap.setWriteGate /
     * the codex map's wiring. A writer may persist a Claude UUID only for a
     * topic whose resolved framework IS claude-code and that is not
     * mid-framework-switch (the durable suppression marker).
     */
    claudeResumeWriteGate(topicId: number | string): {
        allowed: boolean;
        reason?: string;
    };
    codexResumeWriteGate(topicId: number | string): {
        allowed: boolean;
        reason?: string;
    };
    /** Introspection (readout / tests). */
    pendingFor(topicKey: number | string): {
        deferred: boolean;
        changeCount: number;
    } | null;
    suppressionActive(topicKey: number | string): boolean;
    /** Clear timers (tests / shutdown). */
    dispose(): void;
    private shadowObserveSwitch;
    private discloseWrite;
    private flushOverflow;
    private cfg;
    private stamp;
    private audit_;
    private sessionState;
    private matchesExpectedLive;
    private pseudoEquals;
    private recordApplied;
    private setSuppression;
    private clearSuppression;
    private loadDurable;
    private saveDurable;
}
/** The characteristics a spawn against this resolution applies. */
export declare function resolvedToApplied(resolved: ResolvedTopicProfile): AppliedProfile;
export {};
//# sourceMappingURL=TopicProfileOrchestrator.d.ts.map