codex-cli-captain 0.0.9

Codex-Cli-Captain runtime, installer, and MCP server for Codex CLI.
use crate::token_display::format_compact_token_count;
use serde_json::{json, Value};

const CONTEXT_COMPACT_THRESHOLD: u64 = 120_000;
const CONTEXT_NEW_THRESHOLD: u64 = 200_000;
const TOKEN_COMPACT_THRESHOLD: u64 = 200_000;
const ACTIVE_HANDLE_THRESHOLD: u64 = 3;

pub(crate) fn create_long_session_mitigation_payload(
    run_id: &str,
    token_usage: &Value,
    host_subagent_state: &Value,
) -> Value {
    let total_context_tokens = token_usage
        .get("total_context_tokens")
        .and_then(Value::as_u64)
        .unwrap_or(0);
    let total_tokens = token_usage
        .get("total_tokens")
        .and_then(Value::as_u64)
        .unwrap_or(0);
    let active_subagent_count = host_subagent_state
        .get("active_subagent_count")
        .and_then(Value::as_u64)
        .unwrap_or(0);
    let reclaim_needs_attention = host_subagent_state
        .pointer("/reclaim_replan_recommendation/needs_operator_attention")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let active_handle_state = host_subagent_state
        .pointer("/active_handle_cleanup/state")
        .and_then(Value::as_str)
        .unwrap_or("unknown");

    let mut signals = Vec::new();
    if total_context_tokens >= CONTEXT_NEW_THRESHOLD {
        signals.push(json!({
            "kind": "context_pressure",
            "severity": "high",
            "value": total_context_tokens,
            "threshold": CONTEXT_NEW_THRESHOLD,
            "summary": format!(
                "estimated context {} reached the new-session threshold",
                format_compact_token_count(total_context_tokens)
            ),
        }));
    } else if total_context_tokens >= CONTEXT_COMPACT_THRESHOLD {
        signals.push(json!({
            "kind": "context_pressure",
            "severity": "medium",
            "value": total_context_tokens,
            "threshold": CONTEXT_COMPACT_THRESHOLD,
            "summary": format!(
                "estimated context {} reached the compact threshold",
                format_compact_token_count(total_context_tokens)
            ),
        }));
    }
    if total_tokens >= TOKEN_COMPACT_THRESHOLD {
        signals.push(json!({
            "kind": "token_pressure",
            "severity": "medium",
            "value": total_tokens,
            "threshold": TOKEN_COMPACT_THRESHOLD,
            "summary": format!(
                "recorded token usage {} reached the compact threshold",
                format_compact_token_count(total_tokens)
            ),
        }));
    }
    if active_subagent_count >= ACTIVE_HANDLE_THRESHOLD || reclaim_needs_attention {
        signals.push(json!({
            "kind": "resource_pressure",
            "severity": "high",
            "active_subagent_count": active_subagent_count,
            "reclaim_needs_attention": reclaim_needs_attention,
            "summary": "host subagent lifecycle pressure should be checkpointed before continuing",
        }));
    }

    let recommended_action = if signals.iter().any(|signal| {
        signal.get("severity").and_then(Value::as_str) == Some("high")
            && signal.get("kind").and_then(Value::as_str) != Some("token_pressure")
    }) {
        "/new"
    } else if signals.is_empty() {
        "continue"
    } else {
        "/compact"
    };
    let recommended = recommended_action != "continue";
    let reason_codes = signals
        .iter()
        .filter_map(|signal| signal.get("kind").and_then(Value::as_str))
        .map(|value| Value::String(value.to_string()))
        .collect::<Vec<_>>();
    let resume_command = format!("$cap continue {run_id}");
    let checkpoint_command = format!("ccc orchestrate --quiet --json '{{\"run_id\":\"{run_id}\",\"compact\":true,\"resolve_summary\":\"Checkpoint before Codex CLI session rollover.\"}}'");

    json!({
        "recommended": recommended,
        "recommended_action": recommended_action,
        "reason_codes": reason_codes,
        "signals": signals,
        "operator_choice_required": recommended,
        "checkpoint_required": recommended,
        "checkpoint_command": if recommended { Value::String(checkpoint_command) } else { Value::Null },
        "resume_command": if recommended { Value::String(resume_command.clone()) } else { Value::Null },
        "resume_prompt": if recommended {
            Value::String(format!(
                "After the chosen Codex CLI action, resume this CCC run with `{resume_command}`."
            ))
        } else {
            Value::Null
        },
        "slash_command_boundary": "captain recommends /compact, /new, or /exit but does not claim to execute Codex TUI slash commands without Codex CLI or wrapper support",
        "choices": [
            {
                "action": "/compact",
                "when": "summarize the current visible conversation and keep working in the current CLI session"
            },
            {
                "action": "/new",
                "when": "start a fresh conversation in the same CLI after checkpointing the CCC run"
            },
            {
                "action": "/exit",
                "when": "fully restart Codex CLI after checkpointing the CCC run"
            }
        ],
        "policy": {
            "context_compact_threshold": CONTEXT_COMPACT_THRESHOLD,
            "context_new_threshold": CONTEXT_NEW_THRESHOLD,
            "token_compact_threshold": TOKEN_COMPACT_THRESHOLD,
            "active_handle_threshold": ACTIVE_HANDLE_THRESHOLD,
            "active_handle_state": active_handle_state
        }
    })
}

#[cfg(test)]
pub(crate) fn long_session_mitigation_summary(payload: &Value) -> Option<String> {
    let mitigation = payload.get("long_session_mitigation")?;
    if !mitigation
        .get("recommended")
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        return None;
    }
    let action = mitigation
        .get("recommended_action")
        .and_then(Value::as_str)
        .unwrap_or("/compact");
    let reasons = mitigation
        .get("reason_codes")
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .collect::<Vec<_>>()
                .join("+")
        })
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "session_pressure".to_string());
    let resume = mitigation
        .get("resume_command")
        .and_then(Value::as_str)
        .unwrap_or("$cap continue <run_id>");
    Some(format!(
        "Rollover: recommend {action} reason={reasons}; operator choice required; checkpoint before rollover; resume with `{resume}`"
    ))
}

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

    #[test]
    fn long_session_recommends_compact_for_context_pressure() {
        let payload = create_long_session_mitigation_payload(
            "run-123",
            &json!({ "total_context_tokens": 125_000, "total_tokens": 0 }),
            &json!({ "active_subagent_count": 0 }),
        );

        assert_eq!(payload["recommended"], true);
        assert_eq!(payload["recommended_action"], "/compact");
        assert_eq!(payload["operator_choice_required"], true);
        assert_eq!(payload["resume_command"], "$cap continue run-123");
    }

    #[test]
    fn long_session_recommends_new_for_high_resource_pressure() {
        let payload = create_long_session_mitigation_payload(
            "run-456",
            &json!({ "total_context_tokens": 0, "total_tokens": 0 }),
            &json!({ "active_subagent_count": 3 }),
        );

        assert_eq!(payload["recommended"], true);
        assert_eq!(payload["recommended_action"], "/new");
        assert!(payload["slash_command_boundary"]
            .as_str()
            .unwrap()
            .contains("does not claim to execute"));
    }

    #[test]
    fn long_session_has_no_recommendation_when_pressure_is_low() {
        let payload = create_long_session_mitigation_payload(
            "run-789",
            &json!({ "total_context_tokens": 10_000, "total_tokens": 20_000 }),
            &json!({ "active_subagent_count": 0 }),
        );

        assert_eq!(payload["recommended"], false);
        assert_eq!(payload["recommended_action"], "continue");
        assert!(payload["resume_command"].is_null());
    }
}