# API
This library owns durable lifecycle, command, and stop state for one `.session-control` journal.
The crate root re-exports `SessionRecord`, `SessionCommand`, `SessionStopRequest`, `ControlUpdate`, and `ControlProjection`. Their source imports and serialized forms remain unchanged.
```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` projects and encodes one typed update, durably appends it using the existing journal framing, and returns the projected update only after journal synchronization succeeds. Lifecycle projection recursively retains the authoritative control fields, including optional opaque `launchContextNodeIds` and `launchProvenance` values and their occurrences within `historyIngress`. A null `commitReceipt` is removed at every recursive level. Unlisted lifecycle state is discarded. Command and stop updates are unchanged.
`ControlUpdate::projected` performs the same pure lifecycle projection 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 rather than falling back to an older value. 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,
}
```
Record fields are caller-editable values. Lifecycle records use the displayed snake-case JSON field names. Command and stop records use camel-case JSON field names. A false `summary` is omitted during serialization and defaults to false during deserialization. This library does not enforce lifecycle phases, command status transitions, stop scopes, optimistic versions, or idempotency policy.
`compact_directory` enumerates every `.session-control` file in sorted path order and processes one journal at a time. Compaction 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. A journal is replaced and synchronized only when lifecycle projection or survivor selection changes it. Incomplete-tail repair remains observable in the compaction log even when no replacement is required. 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 after removal.
The journal framing, append synchronization, and locking model are unchanged. One handle serializes its own mutable append operations through Rust borrowing and the journal's synchronization. The caller owns transcript coordination, application locking, completion ordering, and cross-handle or cross-process coordination. Independent processes are not coordinated. Operations have no timeout or retry loop.
For a journal containing `N` records, `I` distinct command and stop IDs, and `B` encoded bytes, opening validates `O(B)` input and retains the decoded journal in memory. `projection` performs one ordered scan with `O(N log I)` map work and retains up to `I` typed values. Appending performs `O(J)` encoding work and transient allocation for the update's `J`-sized JSON structure, followed by one durable journal append. Directory compaction sorts `D` directory entries in `O(D log D)` work, then performs one ordered `O(N log I)` survivor scan per matching journal; a changed journal may require `O(N)` additional record storage and one durable replacement. Deletion performs at most one file removal and one directory synchronization. Inputs are limited only by available memory and filesystem capacity; there are no library-added record, JSON, directory-entry, or byte caps.
The integration fixtures exercise append-return and reopen retention, compaction retention and idempotence, open modes, tail repair and corruption rejection, and deletion on an ordinary temporary filesystem. Filesystem latency and synchronization throughput depend on the host and are not given a wall-clock guarantee.
Rollback requires care: an older implementation whose lifecycle allowlist predates `launchContextNodeIds` and `launchProvenance` can discard those fields on append or compaction. Preserve or restore the journal data, or accept that loss, before operating it with such a version.