//! Runtime adapters for preflight maintenance writes.

use agent_doc_document::write_normalization::editor_buffer_preserved_head_exchange;
use agent_doc_frontmatter::frontmatter;
use anyhow::{Context, Result};
use std::path::Path;

use agent_doc_run_context_io::AgentDocContextExt;
use agent_doc_template_io::normalize_user_prompts_in_exchange_safe;

pub struct RuntimePreflightMaintenanceWriteEffects;

pub static PREFLIGHT_MAINTENANCE_WRITE_EFFECTS: RuntimePreflightMaintenanceWriteEffects =
    RuntimePreflightMaintenanceWriteEffects;

pub struct RuntimePreflightCycleCompletionEffects<
    RepairIoEffects,
    ReplayWriteEffects,
    SessionCheckEffects,
> where
    RepairIoEffects: agent_doc_repair_io::RepairIoEffects
        + agent_doc_repair_io::RepairTemplateWriteEffects
        + 'static,
    ReplayWriteEffects: agent_doc_repair_io::RepairReplayWriteEffects + 'static,
    SessionCheckEffects: agent_doc_session_check_io::SessionCheckEffects,
{
    repair_effects:
        agent_doc_repair_io::RepairCoordinatorEffects<'static, RepairIoEffects, ReplayWriteEffects>,
    session_check_effects: SessionCheckEffects,
}

pub fn preflight_cycle_completion_effects<
    RepairIoEffects,
    ReplayWriteEffects,
    SessionCheckEffects,
>(
    repair_effects: agent_doc_repair_io::RepairCoordinatorEffects<
        'static,
        RepairIoEffects,
        ReplayWriteEffects,
    >,
    session_check_effects: SessionCheckEffects,
) -> RuntimePreflightCycleCompletionEffects<RepairIoEffects, ReplayWriteEffects, SessionCheckEffects>
where
    RepairIoEffects: agent_doc_repair_io::RepairIoEffects
        + agent_doc_repair_io::RepairTemplateWriteEffects
        + 'static,
    ReplayWriteEffects: agent_doc_repair_io::RepairReplayWriteEffects + 'static,
    SessionCheckEffects: agent_doc_session_check_io::SessionCheckEffects,
{
    RuntimePreflightCycleCompletionEffects {
        repair_effects,
        session_check_effects,
    }
}

impl<RepairIoEffects, ReplayWriteEffects, SessionCheckEffects>
    agent_doc_preflight_io::PreflightCycleCompletionEffects
    for RuntimePreflightCycleCompletionEffects<
        RepairIoEffects,
        ReplayWriteEffects,
        SessionCheckEffects,
    >
where
    RepairIoEffects: agent_doc_repair_io::RepairIoEffects
        + agent_doc_repair_io::RepairTemplateWriteEffects
        + 'static,
    ReplayWriteEffects: agent_doc_repair_io::RepairReplayWriteEffects + 'static,
    SessionCheckEffects: agent_doc_session_check_io::SessionCheckEffects,
{
    fn repair(&self, file: &Path) -> Result<agent_doc_turn::repair::RepairOutcome> {
        agent_doc_repair_io::run(self.repair_effects, file)
    }

    fn commit(&self, file: &Path) -> Result<bool> {
        agent_doc_commit_io::commit(file)
    }

    fn recover_retained_document_write(&self, file: &Path) -> Result<bool> {
        agent_doc_document_realtime_io::recover_retained_document_write_before_new_cycle(
            file,
            agent_doc_document_realtime_io::RetainedWriteCycleBoundary::Preflight,
        )
    }

    fn retained_document_write(&self, file: &Path) -> bool {
        // `#retainedsettlereactive`: ask the shared derived verdict, not the raw
        // `pending_document_write(..).is_some()`. That reload arbitrated the
        // gate from storage (`#lazily-hot-path`) and reported *any* retained
        // intent as outstanding — including one the converged document had
        // already satisfied, which `session-check` simultaneously reported as
        // ok. Both being true is what deadlocked the session.
        //
        // `#retainedclearreactive`: sharing the *cell* was necessary but not
        // sufficient, and `#preflightsettleparity`'s fix — a second
        // `settle_retained_document_write` call beside session-check's — was
        // the imperative form of the answer. The clear is now an `Effect` in
        // the controller's per-document graph gated on this same verdict cell,
        // so reading the gate is what settles a `Satisfied` intent and there is
        // no companion call for a third consumer to omit.
        agent_doc_document_realtime_io::retained_write_blocks_new_cycle(
            file,
            agent_doc_document_realtime_io::RetainedWriteCycleBoundary::Preflight.gate_source(),
        )
    }

    fn session_interruption(&self, file: &Path) -> Result<Option<String>> {
        match agent_doc_session_check_io::inspect(file, &self.session_check_effects)? {
            agent_doc_session_check_io::SessionCheckStatus::Ok(_) => Ok(None),
            agent_doc_session_check_io::SessionCheckStatus::Interrupted(reason) => Ok(Some(reason)),
        }
    }

    fn detect_bypassed_response_write(&self, file: &Path) -> Result<Option<String>> {
        agent_doc_session_check_io::detect_bypassed_response_write(file)
    }
}

impl agent_doc_preflight_io::PreflightMaintenanceWriteEffects
    for RuntimePreflightMaintenanceWriteEffects
{
    fn record_document_write_provenance(&self, file: &Path, content: &str) {
        agent_doc_document_realtime_io::record_document_write_provenance(file, content);
    }

    fn guard_visible_write_expected_current(
        &self,
        file: &Path,
        source: &str,
        expected_current: &str,
    ) -> Result<()> {
        agent_doc_document_realtime_io::guard_visible_write_expected_current(
            file,
            source,
            expected_current,
        )
    }

    fn converge_or_disk_write(
        &self,
        file: &Path,
        current_content: &str,
        target_content: &str,
        source: &str,
    ) -> Result<()> {
        agent_doc_write_converge_io::converge_or_disk_write(
            &agent_doc_document_realtime_io::RUNTIME_WRITE_CONVERGENCE_EFFECTS,
            file,
            current_content,
            target_content,
            source,
        )
    }
}

pub fn resolve_current_preflight_document(file: &Path, source: &str) -> Result<String> {
    Ok(
        agent_doc_document_realtime_io::try_resolve_current_doc_from_file_with_source(file, source)
            .with_context(|| {
                format!(
                    "preflight {source}: resolve current document {}",
                    file.display()
                )
            })?
            .content,
    )
}

pub fn enforce_no_uncommitted_closeout_drift(
    file: &Path,
    rc: &agent_doc_run_context_io::CycleContext,
    session_check_effects: &impl agent_doc_session_check_io::SessionCheckEffects,
) -> Result<()> {
    if recover_route_queue_snapshot_commit_boundary(file, rc)? {
        return Ok(());
    }

    if let Some(replay) =
        agent_doc_session_check_io::detect_jb_cache_conflict_accept_duplicate_replay_with_context(
            file, rc,
        )?
    {
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "jb_cache_conflict_accept_duplicate_replay_repaired file={} heading={}",
                file.display(),
                replay.heading.replace('\n', " ")
            ),
        );
        eprintln!(
            "[preflight] jb_cache_conflict_accept: removing duplicate response replay at `{}` for {}",
            replay.heading,
            file.display()
        );
        agent_doc_document_realtime_io::atomic_write_through_authority(
            file,
            &replay.deduped_content,
        )?;
        agent_doc_snapshot_io::checkpoint_document_baseline(
            file,
            &replay.deduped_content,
            agent_doc_ops_log_io::log_op,
        )?;
        return Ok(());
    }

    if let Some(overapplication) =
        agent_doc_session_check_io::detect_late_ipc_response_overapplication_with_context(file, rc)?
    {
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "late_ipc_response_overapplication_repaired file={}",
                file.display()
            ),
        );
        eprintln!(
            "[preflight] late_ipc_overapplication: restoring committed HEAD over re-added response for {}",
            file.display()
        );
        agent_doc_document_realtime_io::atomic_write_through_authority(
            file,
            &overapplication.remediated_content,
        )?;
        agent_doc_snapshot_io::checkpoint_document_baseline(
            file,
            &overapplication.remediated_content,
            agent_doc_ops_log_io::log_op,
        )?;
        return Ok(());
    }

    if agent_doc_session_check_io::detect_jb_cache_conflict_cancel_recoverable_with_context(
        file, rc,
    )? {
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "jb_cache_conflict_cancel_auto_recovery_attempt file={}",
                file.display()
            ),
        );
        eprintln!(
            "[preflight] jb_cache_conflict_cancel: response written but not committed for {} -- running auto-commit",
            file.display()
        );
        match agent_doc_commit_io::commit(file) {
            Ok(_) => {
                rc.invalidate_head_content();
                agent_doc_ops_log_io::log_op(
                    file,
                    &format!(
                        "jb_cache_conflict_cancel_auto_recovery_succeeded file={}",
                        file.display()
                    ),
                );
                return Ok(());
            }
            Err(e) => {
                agent_doc_ops_log_io::log_op(
                    file,
                    &format!(
                        "jb_cache_conflict_cancel_auto_recovery_failed file={} error={}",
                        file.display(),
                        e.to_string().replace('\n', " ")
                    ),
                );
                eprintln!(
                    "[preflight] jb_cache_conflict_cancel auto-commit failed for {}: {}",
                    file.display(),
                    e
                );
            }
        }
    }

    if recover_ipc_truncated_worktree_from_editor_buffer(file, rc)? {
        return Ok(());
    }

    if let Some(message) =
        agent_doc_session_check_io::detect_uncommitted_closeout_drift_with_context(
            file,
            rc,
            session_check_effects,
        )?
    {
        // `#hookcontractlost`: bailing here is what made the wedge self-sustaining.
        // Preflight owns recovery *before* diffing, so this error aborts the hook
        // before it can emit a contract — and the remedy the message names
        // (`agent-doc commit`) can only be run by the operator, because the agent
        // is correctly instructed never to shell preflight itself. Nothing else
        // clears the drift, so every following turn hits the same bail.
        //
        // When the drift is document-only there is no response body at risk, so
        // run that remedy in place instead of printing it. Mirrors the
        // jb_cache_conflict_cancel auto-commit above.
        if session_check_effects.document_only_drift_is_commit_recoverable(file) {
            agent_doc_ops_log_io::log_op(
                file,
                &format!(
                    "preflight_document_only_drift_auto_commit_attempt file={}",
                    file.display()
                ),
            );
            eprintln!(
                "[preflight] document-only closeout drift for {} -- running binary-owned auto-commit",
                file.display()
            );
            match agent_doc_commit_io::commit(file) {
                Ok(_) => {
                    rc.invalidate_head_content();
                    agent_doc_ops_log_io::log_op(
                        file,
                        &format!(
                            "preflight_document_only_drift_auto_commit_succeeded file={}",
                            file.display()
                        ),
                    );
                    return Ok(());
                }
                Err(err) => {
                    agent_doc_ops_log_io::log_op(
                        file,
                        &format!(
                            "preflight_document_only_drift_auto_commit_failed file={} error={}",
                            file.display(),
                            err.to_string().replace('\n', " ")
                        ),
                    );
                    eprintln!(
                        "[preflight] document-only drift auto-commit failed for {}: {}",
                        file.display(),
                        err
                    );
                }
            }
        }
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "preflight_blocked_uncommitted_closeout_drift file={} reason={}",
                file.display(),
                message.replace('\n', " ")
            ),
        );
        anyhow::bail!("{}", message);
    }
    Ok(())
}

pub fn relocate_out_of_exchange_prompt_before_diff(file: &Path) -> Result<bool> {
    let doc_content = resolve_current_preflight_document(file, "prompt_tail_relocation")?;
    let (frontmatter, _) = frontmatter::parse(&doc_content).map_err(|err| {
        anyhow::anyhow!(
            "failed to parse document frontmatter {}: {err}",
            file.display()
        )
    })?;
    if !frontmatter.resolve_mode().is_template() {
        return Ok(false);
    }

    let Some(mut repaired) = agent_doc_template::repair_prompt_tail_outside_exchange(&doc_content)?
    else {
        return Ok(false);
    };

    if let Some(snapshot_content) = agent_doc_snapshot_io::load_document_baseline(file)? {
        repaired =
            normalize_user_prompts_in_exchange_safe(&repaired, &repaired, &snapshot_content, file);
        repaired = agent_doc_template_io::normalize_template_structure_or_fail(&repaired, file)?;
    }

    if repaired == doc_content {
        return Ok(false);
    }

    agent_doc_document_realtime_io::atomic_write_through_authority(file, &repaired)?;
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "preflight_repair_prompt_tail_outside_exchange file={}",
            file.display()
        ),
    );
    eprintln!(
        "[preflight] repaired prompt tail outside exchange in {}",
        file.display()
    );
    Ok(true)
}

pub fn remove_duplicate_answered_exchange_prompt_tail_for_preflight(file: &Path) -> Result<bool> {
    let current = resolve_current_preflight_document(file, "duplicate_answered_prompt_tail")?;
    let Some(cleaned_doc) =
        agent_doc_template::remove_duplicate_answered_exchange_prompt_tail(&current)
    else {
        return Ok(false);
    };

    agent_doc_document_realtime_io::atomic_write_through_authority(file, &cleaned_doc)?;
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "duplicate_answered_exchange_prompt_tail_removed file={} source=preflight",
            file.display()
        ),
    );
    eprintln!(
        "[preflight] removed duplicate answered prompt tail after exchange boundary in {}",
        file.display()
    );
    Ok(true)
}

pub fn remove_post_exchange_duplicate_prompt_comments_for_preflight(
    file: &Path,
    rc: &agent_doc_run_context_io::CycleContext,
) -> Result<bool> {
    let current = resolve_current_preflight_document(file, "duplicate_prompt_comments")?;
    let snapshot_doc = agent_doc_snapshot_io::load_document_baseline(file)
        .ok()
        .flatten();
    let head_doc = rc.head_content();
    let mut preserve_docs = Vec::new();
    preserve_docs.push(current.as_str());
    if let Some(head_doc) = head_doc.as_deref() {
        preserve_docs.push(head_doc.as_str());
    }
    if let Some(snapshot_doc) = snapshot_doc.as_deref() {
        preserve_docs.push(snapshot_doc);
    }
    let Some(cleaned_doc) =
        agent_doc_template::remove_post_exchange_duplicate_prompt_comments_preserving_docs(
            &current,
            &preserve_docs,
        )
    else {
        return Ok(false);
    };

    agent_doc_document_realtime_io::atomic_write_through_authority(file, &cleaned_doc)?;
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "post_exchange_duplicate_prompt_comment_removed file={} source=preflight",
            file.display()
        ),
    );
    eprintln!(
        "[preflight] scrubbed duplicate prompt text from comment after exchange in {}",
        file.display()
    );
    Ok(true)
}

pub fn recover_route_queue_snapshot_commit_boundary(
    file: &Path,
    rc: &agent_doc_run_context_io::CycleContext,
) -> Result<bool> {
    if !detect_route_queue_snapshot_commit_boundary_recoverable(file, rc)? {
        return Ok(false);
    }
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "route_queue_snapshot_auto_recovery_attempt file={}",
            file.display()
        ),
    );
    eprintln!(
        "[preflight] route_queue_snapshot: queued dispatch snapshot is not committed for {}; running auto-commit",
        file.display()
    );
    match agent_doc_commit_io::commit(file) {
        Ok(_) => {
            rc.invalidate_head_content();
            agent_doc_ops_log_io::log_op(
                file,
                &format!(
                    "route_queue_snapshot_auto_recovery_succeeded file={}",
                    file.display()
                ),
            );
            Ok(true)
        }
        Err(e) => {
            agent_doc_ops_log_io::log_op(
                file,
                &format!(
                    "route_queue_snapshot_auto_recovery_failed file={} error={}",
                    file.display(),
                    e.to_string().replace('\n', " ")
                ),
            );
            eprintln!(
                "[preflight] route_queue_snapshot auto-commit failed for {}: {}",
                file.display(),
                e
            );
            Ok(false)
        }
    }
}

pub fn detect_route_queue_snapshot_commit_boundary_recoverable(
    file: &Path,
    rc: &agent_doc_run_context_io::CycleContext,
) -> Result<bool> {
    let Some(state) = agent_doc_cycle_state_io::load_with_closeout_projection(file)? else {
        return Ok(false);
    };
    if state.is_open() {
        return Ok(false);
    }
    if !matches!(
        rc.snapshot_commit_status(),
        agent_doc_snapshot_io::SnapshotCommitStatus::SnapshotDiffersFromHead { .. }
    ) {
        return Ok(false);
    }

    let Some(snapshot) = rc.snapshot_content() else {
        return Ok(false);
    };
    let Some(head) = rc.head_content() else {
        return Ok(false);
    };
    if agent_doc_turn::document_drift::detect_bypassed_response_write_between(&head, &snapshot)
        .is_some()
    {
        return Ok(false);
    }

    let snapshot_prompts =
        agent_doc_queue::route_dispatch::active_auto_route_queue_prompt_texts(&snapshot)?;
    let head_prompts =
        agent_doc_queue::route_dispatch::active_auto_route_queue_prompt_texts(&head)?;
    // Recover only genuine active-auto-queue commit-boundary churn: either the
    // snapshot still carries the queued dispatch (enqueue case) or HEAD carried
    // an active auto-queue that the snapshot has since drained to inactive
    // residue via queue maintenance (#drained-done-queue-clear). The drained
    // case reduces to an empty stripped diff below (queue body + `queue_active`
    // are both stripped before comparison), so it auto-commits only when no
    // non-queue user change exists. Bail on any other snapshot/HEAD drift.
    if snapshot_prompts.is_empty() && head_prompts.is_empty() {
        return Ok(false);
    }

    let head_norm =
        agent_doc_queue::route_dispatch::strip_route_queue_state_for_boundary_compare(&head);
    let snapshot_norm =
        agent_doc_queue::route_dispatch::strip_route_queue_state_for_boundary_compare(&snapshot);
    let Some(diff_text) = agent_doc_diff::unified_diff_from_contents(&head_norm, &snapshot_norm)
    else {
        return Ok(true);
    };
    let changes = agent_doc_diff::classify_prompt_bearing_changes(&diff_text)
        .into_iter()
        .filter(|change| {
            !matches!(
                change.kind,
                agent_doc_diff::PromptBearingChangeKind::RecoveryArtifact
                    | agent_doc_diff::PromptBearingChangeKind::BoundaryArtifact
            )
        })
        .collect::<Vec<_>>();
    if changes.is_empty() {
        return Ok(true);
    }

    Ok(changes.iter().all(|change| {
        change.kind == agent_doc_diff::PromptBearingChangeKind::PromptTarget
            && agent_doc_queue::route_dispatch::route_prompt_text_for_change(&change.text)
                .is_some_and(|text| snapshot_prompts.iter().any(|prompt| prompt == &text))
    }))
}

/// `#ipctruncrecover`: reconcile an IPC-truncated working tree from the live editor
/// buffer instead of bailing the layout guard.
///
/// Returns `Ok(true)` only when the editor's normal full-state projection has
/// already converged through both visible authority and disk, preserves HEAD's
/// committed `exchange`, and permits resetting the snapshot to HEAD. It never
/// requests an editor save. Returns `Ok(false)` while projection is pending.
pub fn recover_ipc_truncated_worktree_from_editor_buffer(
    file: &Path,
    rc: &agent_doc_run_context_io::CycleContext,
) -> Result<bool> {
    if !matches!(
        rc.snapshot_commit_status(),
        agent_doc_snapshot_io::SnapshotCommitStatus::SnapshotDiffersFromHead { .. }
    ) {
        return Ok(false);
    }
    if agent_doc_session_check_io::detect_unstarted_prompt_bearing_diff(file)?.is_some() {
        return Ok(false);
    }
    let Some(head) = rc.head_content() else {
        return Ok(false);
    };
    let Ok(canonical) = file.canonicalize() else {
        return Ok(false);
    };
    let lazily_current = agent_doc_crdt_relay_io::current_text_for_file(&canonical)?;
    let (authority_state, delivery_converged) = match &lazily_current {
        agent_doc_crdt_relay_io::CurrentText::Detached => ("detached", true),
        agent_doc_crdt_relay_io::CurrentText::Current {
            delivery_converged, ..
        } => ("current", *delivery_converged),
        agent_doc_crdt_relay_io::CurrentText::EditorAttachedMissingReplica => {
            ("editor_attached_missing_replica", false)
        }
        agent_doc_crdt_relay_io::CurrentText::EditorSyncPending => ("editor_sync_pending", false),
    };
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "lazily_authority_observed file={} transition=ipc_truncation_recover state={} delivery_converged={}",
            file.display(),
            authority_state,
            delivery_converged,
        ),
    );
    if !delivery_converged {
        return Ok(false);
    }
    let agent_doc_crdt_relay_io::CurrentText::Current {
        text: projected, ..
    } = lazily_current
    else {
        return Ok(false);
    };
    let disk = std::fs::read_to_string(&canonical).with_context(|| {
        format!(
            "failed to read projected disk cut for {}",
            canonical.display()
        )
    })?;
    if disk != projected {
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "ipc_truncation_recover_pending file={} visible_hash={} disk_hash={} driver=state_projection",
                file.display(),
                agent_doc_hash::content_hash(&projected),
                agent_doc_hash::content_hash(&disk),
            ),
        );
        return Ok(false);
    }

    let flushed = projected;
    if !editor_buffer_preserved_head_exchange(&flushed, &head) {
        agent_doc_ops_log_io::log_op(
            file,
            &format!(
                "ipc_truncation_recover_rejected file={} reason=editor_buffer_lost_committed_exchange flushed_len={} head_len={}",
                file.display(),
                flushed.len(),
                head.len()
            ),
        );
        return Ok(false);
    }

    agent_doc_snapshot_io::checkpoint_document_baseline(file, &head, agent_doc_ops_log_io::log_op)?;
    rc.invalidate_snapshot_content();
    agent_doc_ops_log_io::log_op(
        file,
        &format!(
            "ipc_truncation_recovered_from_editor_projection file={} projected_len={} head_len={} content_hash={} driver=state_projection",
            file.display(),
            flushed.len(),
            head.len(),
            agent_doc_hash::content_hash(&flushed),
        ),
    );
    eprintln!(
        "[preflight] #ipctruncrecover: reconciled IPC-truncated working tree from the live editor buffer for {} (no git recovery needed)",
        file.display()
    );
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn seed_reliable_sync_open(file: &Path, tag: &str) {
        let document_hash = agent_doc_hash::document_id_for_path(file);
        agent_doc_reliable_sync_io::global_liveness_plane()
            .lock()
            .restore_liveness(&[agent_doc_reliable_sync_io::liveness::LivenessOp::Open {
                document_hash,
                pid: std::process::id().into(),
                tag: tag.to_string(),
            }]);
    }

    /// Preflight must not adopt disk after an attached editor's bounded replica
    /// refresh exhausts. Even a read can feed stale structure into later cycle
    /// mutations, so the authority boundary fails closed before that happens.
    #[test]
    fn preflight_missing_editor_model_refuses_disk_while_editor_is_attached() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".agent-doc")).unwrap();
        let file = dir.path().join("session.md");
        let disk = "---\nagent: codex\n---\n\nbody\n";
        std::fs::write(&file, disk).unwrap();
        agent_doc_crdt_relay_io::register_embedded_relay_route_for_file(&file).unwrap();
        let file = file.canonicalize().unwrap();
        let owner = "preflight-missing-editor-model-test";
        seed_reliable_sync_open(&file, owner);

        let err = resolve_current_preflight_document(&file, "test")
            .expect_err("attached editor authority must fail closed");
        assert!(
            format!("{err:#}").contains("disk read authority is refused"),
            "unexpected refusal: {err:#}"
        );

        let ops_log = std::fs::read_to_string(dir.path().join(".agent-doc/logs/ops.log")).unwrap();
        assert!(
            ops_log.contains("realtime_doc_resolve_disk_read_refused")
                && ops_log.contains("upstream_replica_refresh_exhausted"),
            "the refusal must record the exhausted attached-editor recovery:\n{ops_log}"
        );
        // Recover BEFORE refusing. For the MISSING-REPLICA family that recovery
        // is the upstream self-heal (`#bn41`/`#px82`), which re-registers and
        // re-observes with backoff before resolution ever reaches the disk tier.
        // The read-path model rebuild is deliberately NOT run again here — it
        // would duplicate that work at ~28s per read site, which multiplied
        // across a compact/commit's several resolutions and blew their timeout.
        // The rebuild is scoped to sync-pending, which has no upstream self-heal
        // (see `current_resolve_rebuilds_editor_model_when_sync_pending`).
        assert!(
            ops_log.contains("editor_replica_reregister_attempt"),
            "the refusal must be preceded by the upstream replica self-heal:\n{ops_log}"
        );
        assert!(!ops_log.contains("realtime_doc_resolve_disk_read_fallback"));
    }

    #[test]
    fn preflight_missing_editor_model_recovers_after_delayed_replica_registration() {
        let dir = tempfile::TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".agent-doc")).unwrap();
        let file = dir.path().join("session.md");
        let disk = "---\nagent: codex\n---\n\nbody\n";
        std::fs::write(&file, disk).unwrap();
        agent_doc_crdt_relay_io::register_embedded_relay_route_for_file(&file).unwrap();
        let file = file.canonicalize().unwrap();
        let owner = "preflight-missing-editor-model-recover-test";
        seed_reliable_sync_open(&file, owner);

        let file_for_register = file.clone();
        let register = std::thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(50));
            agent_doc_crdt_relay_io::register_replica_for_file(
                &file_for_register,
                "intellij:preflight-recover",
            )
            .expect("delayed register should not fail")
            .expect("editor-attached register should allocate model");
        });

        // `#bn41`/`#px82`: the resolve now re-registers the replica and
        // re-observes within a bounded attempt budget, so a replica that appears
        // shortly after the first observation is picked up by the SAME call.
        // This used to fail the first call and only recover on a later run,
        // which is what forced the manual `admin recycle` + `admin reload-lib`
        // pair when the failure was really just intermittent.
        let current = resolve_current_preflight_document(&file, "test_preflight_recover")
            .expect("preflight should self-heal once the replica registers");
        assert_eq!(current, disk);
        register.join().unwrap();
        let current = resolve_current_preflight_document(&file, "test_preflight_recovered")
            .expect("preflight should resume after replica registration");
        assert_eq!(current, disk);

        let ops_log = std::fs::read_to_string(dir.path().join(".agent-doc/logs/ops.log")).unwrap();
        assert!(
            !ops_log.contains("realtime_doc_resolve_disk_fallback"),
            "preflight must not use disk while the attached model is recovering:\n{ops_log}"
        );
        assert!(
            !ops_log.contains("document_model_ensure_start"),
            "preflight current-doc reads must not enter document-model ensure:\n{ops_log}"
        );
        assert!(
            !ops_log.contains("preflight_current_document_local_relay_unavailable"),
            "preflight must not use the stale local fast-fail guard:\n{ops_log}"
        );
    }
}
