kcode-session-control-state 0.1.0

Typed session-control projection, append, compaction, and deletion
Documentation
# API

`kcode-session-control-state` owns typed lifecycle, command, and stop state for one `.session-control` journal.

```rust
pub enum OpenMode {
    CreateNew,
    OpenOrCreate,
    ExistingOnly,
}

pub enum ControlUpdate {
    Lifecycle(SessionRecord),
    Command(SessionCommand),
    StopRequest(SessionStopRequest),
}

impl ControlUpdate {
    pub fn projected(self) -> ControlUpdate;
}

pub struct ControlProjection {
    pub lifecycle: Option<SessionRecord>,
    pub commands: std::collections::BTreeMap<String, SessionCommand>,
    pub stop_requests: std::collections::BTreeMap<String, SessionStopRequest>,
}

impl SessionControl {
    pub fn open(
        directory: impl AsRef<std::path::Path>,
        session_id: &str,
        mode: OpenMode,
    ) -> anyhow::Result<Option<SessionControl>>;

    pub fn projection(&self) -> ControlProjection;

    pub fn append(
        &mut self,
        recorded_at: impl Into<String>,
        update: ControlUpdate,
    ) -> anyhow::Result<ControlUpdate>;

    pub fn delete(self) -> anyhow::Result<()>;

    pub fn compact_directory(
        directory: impl AsRef<std::path::Path>,
    ) -> anyhow::Result<()>;
}
```

`CreateNew` fails if the derived control file exists. `OpenOrCreate` opens or creates it. `ExistingOnly` returns `None` only when it is absent. The directory must already exist. The handle and derived path are opaque.

`append` synchronizes one typed update before returning it. Lifecycle updates are recursively reduced to authoritative control fields; `ControlUpdate::projected` performs the same reduction without persistence. Projection selects the latest lifecycle record and the latest valid command and stop value per ID. A malformed latest lifecycle record yields no lifecycle; malformed command and stop values are ignored.

```rust
pub struct SessionRecord {
    pub id: String,
    pub phase: String,
    pub started_at: String,
    pub updated_at: String,
    pub state: serde_json::Value,
    pub provenance_id: Option<String>,
    pub version: i64,
    pub last_user_message_at: Option<String>,
    pub ended_at: Option<String>,
    pub ingress_failure_count: i64,
    pub ingress_failures: serde_json::Value,
    pub ingress_next_attempt_at: Option<String>,
    pub summary: bool,
}

pub struct SessionCommand {
    pub id: String,
    pub conversation_id: String,
    pub sequence: i64,
    pub kind: String,
    pub payload: serde_json::Value,
    pub status: String,
    pub cancel_requested: bool,
    pub outcome: Option<serde_json::Value>,
    pub created_at: String,
    pub processing_started_at: Option<String>,
    pub completed_at: Option<String>,
    pub idempotency_id: String,
}

pub struct SessionStopRequest {
    pub id: String,
    pub session_id: String,
    pub scope: String,
    pub status: String,
    pub outcome: Option<serde_json::Value>,
    pub requested_at: String,
    pub completed_at: Option<String>,
    pub idempotency_id: String,
}
```

These record fields are caller-editable values; this leaf does not enforce lifecycle phases, command status transitions, stop scopes, optimistic versions, or idempotency policy.

`compact_directory` opens every `.session-control` file in sorted path order. It retains the latest lifecycle record, latest record per command and stop ID, and every unknown-kind record in original survivor order. A typed command or stop record without a string `id` rejects compaction. Call compaction only at an application-defined startup boundary.

Opening repairs and synchronizes only an incomplete final tail. Complete corruption rejects opening. Deletion consumes the handle, closes it before removing the file, and synchronizes the directory.

The caller owns transcript coordination, application locking, completion ordering, and cross-handle coordination. Independent processes are not coordinated.