use anyhow::{anyhow, Result};
use serde_json::Value;
use super::correlation_id;
use super::identity::{
field_error, required_non_empty_string, validate_identity_with_required_conversation,
validate_workspace_root,
};
use super::input::{parse_outer_object, require_event_name};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorStopStatus {
Completed,
Aborted,
}
impl CursorStopStatus {
pub fn as_str(&self) -> &'static str {
match self {
CursorStopStatus::Completed => "completed",
CursorStopStatus::Aborted => "aborted",
}
}
}
#[derive(Debug, Clone)]
pub struct CursorStopEvent {
pub session_id: String,
pub workspace_root: String,
pub status: CursorStopStatus,
pub generation_id: String,
pub loop_count: u64,
pub transcript_path: Option<String>,
}
impl CursorStopEvent {
pub fn canonical_stop_key(&self) -> String {
format!(
"{}:{}:{}",
self.session_id, self.generation_id, self.loop_count
)
}
}
pub fn parse_stop_event(bytes: &[u8]) -> Result<CursorStopEvent> {
let object = parse_outer_object(bytes)?;
require_event_name(&object, "stop")?;
let session_id = validate_identity_with_required_conversation(&object)?;
let workspace_root = validate_workspace_root(&object)?;
let status = validate_stop_status(&object)?;
let generation_id = required_non_empty_string(&object, "generation_id")?;
let loop_count = validate_loop_count(&object)?;
let transcript_path = stop_transcript_path_field(&object)?;
Ok(CursorStopEvent {
session_id,
workspace_root,
status,
generation_id,
loop_count,
transcript_path,
})
}
fn validate_stop_status(object: &serde_json::Map<String, Value>) -> Result<CursorStopStatus> {
let status = required_non_empty_string(object, "status")?;
match status.as_str() {
"completed" => Ok(CursorStopStatus::Completed),
"aborted" => Ok(CursorStopStatus::Aborted),
other => Err(anyhow!(
"cursor stop status '{other}' is outside the approved set \
(completed, aborted); unobserved statuses fail closed with zero \
writes [correlation_id={}]",
correlation_id()
)),
}
}
fn validate_loop_count(object: &serde_json::Map<String, Value>) -> Result<u64> {
match object.get("loop_count") {
None => Err(field_error("loop_count", "missing")),
Some(Value::Null) => Err(field_error("loop_count", "null")),
Some(Value::Number(number)) => number
.as_u64()
.ok_or_else(|| field_error("loop_count", "not a non-negative integer")),
Some(_) => Err(field_error("loop_count", "wrong type")),
}
}
fn stop_transcript_path_field(object: &serde_json::Map<String, Value>) -> Result<Option<String>> {
match object.get("transcript_path") {
None | Some(Value::Null) => Ok(None),
Some(Value::String(path)) => Ok(Some(path.clone())),
Some(_) => Err(field_error("transcript_path", "wrong type")),
}
}