Skip to main content

Crate hotl_engine

Crate hotl_engine 

Source
Expand description

L3 — the turn engine, M1: actor + turn tasks (commit-protocol.md).

One session actor per session is the sole committer to the log and the owner of the projection ([actor]); turn tasks read actor-granted snapshots at sample boundaries and propose entries ([turn]). Steers admitted mid-turn are woven into the next sample (the conflict table’s rebase row); interrupts travel out-of-band via a shared token; permission asks are events carrying a oneshot reply.

Re-exports§

pub use hooks::NotificationKind;

Modules§

hooks
Extension hooks (M5). The engine consults a Hooks impl at two points in the tool phase — before a call runs and after it returns — scoped to the events actually used, not a 35-event schema (Delivery #5).

Structs§

CommitAck
What the writer acked with, as a proposer sees it — the shipped hotl_store::Ack, renamed at this boundary to match commit-protocol.md §Vocabulary.
CommitTicket
Handed back the moment the actor forwards a proposal to the writer in AckMode::Pipelined (commit-protocol.md §Vocabulary). id and seq are carried eagerly — the actor mints the ulid and assigns seq at validation, before the write — so a proposer knows its own identity and its own commit order without waiting for durability. Only durability waits, and ack is the one place a commit failure is ever reported.
EngineConfig
LedgerSummary
Re-exported so hotl_engine::LedgerSummary resolves alongside EngineEvent::LedgerReport — the loop-overhead instrument (§S1) lives in its own module ([ledger]) since it is self-contained (no dependency on the rest of the engine’s types) and independently unit-tested. What gets flushed at TurnFinished (as an crate::EngineEvent::LedgerReport, never the canon log) and what a CI regression gate consumes.
PhaseDeltaSummary
Re-exported so hotl_engine::LedgerSummary resolves alongside EngineEvent::LedgerReport — the loop-overhead instrument (§S1) lives in its own module ([ledger]) since it is self-contained (no dependency on the rest of the engine’s types) and independently unit-tested. One attributable stage’s timing across every sample in a flush.
PreparedEntry
One entry a turn task proposes to the actor, already serialized and masked (commit-protocol.md §Proposal payloads): the actor’s disk-write path (hotl_store::SessionLog::append_prepared) never touches EntryPayload again. item carries the typed value for entries that also live in the model-visible projection — the actor still needs it to update SessionCmd::Snapshot’s answer, and keeping it here is not a second serialization: it is the exact value the turn already built in memory to produce payload, never re-parsed from payload’s bytes (that would just move T3-16’s per-entry cost back onto the actor rather than delete it). None for entries that never enter the projection (Usage, PendingAsk/AskResolved).
ProjectionHead
Re-exported so hotl_engine::ProjectionHead names what SessionHandle::head hands out — the epoch-fenced published projection (commit-protocol.md §Read invariant). It lives in [actor], next to the only thing that may publish it. The projection head the actor publishes (commit-protocol.md §Read invariant, amendment 2): epoch-fenced, published only post-ack, and read by turn tasks through a watch::Receiver they cannot write. The actor is the sole publisher — the watch::Sender never leaves [Head], which only [run] owns.
SessionDeps
SessionHandle
Snapshot
Re-exported alongside ProjectionHead: what a read of it yields, split into the durable projection and the ephemeral per-sample tail. Out-of-crate readers (fork’s history seed) name it to say which half they take. One read of the projection head, split by durability.
SpecDigest
A compaction digest computed speculatively during the turn, overlapping the summarize call with the turn’s own samples. Indices refer to the projection the digest was planned against; the projection only appends between folds, so they stay valid until the fold that consumes them.
TurnContinuation
The per-turn state a compaction respawn must NOT reset (T2-2). A fold is “no new user item, same logical turn” — so every counter that bounds that turn has to cross it. Reconstructing a Turn from Default here is what let max_turns be defeated by the very scenario it exists for. INVARIANT: every per-turn safety counter survives a compaction respawn. Enforced by max_turns_is_enforced_across_a_compaction and three_folds_with_progress_do_not_exhaust_the_streak.

Enums§

AckMode
Whether the proposer waits for durability (commit-protocol.md §Vocabulary). Orthogonal to hotl_store::AckTier, which is how durable the write must be before the writer acks: canon is Durable in both modes, and only who waits changes.
AskReply
A human’s answer to a permission ask. Widened from a bare bool so a denial can carry the reason to the model as tool-result feedback — a steer fused with a “no”. §2b (M4) extends this with AllowEdited/Respond; callers should treat it as non-exhaustive.
CommitFailed
Why a pipelined commit will never be durable. Exactly two variants (commit-protocol.md §Vocabulary).
EngineEvent
Everything the surface renders. Ask carries the reply channel — the surface (or an allow-rule upstream) is the human on the loop.
EntryProposal
A batch of entries a turn task asks the actor to commit (commit-protocol.md §Vocabulary), in the two shapes the protocol names. Both answer with exactly one CommitTicket in AckMode::Pipelined; they differ in what reaches the writer.
Outcome
Phase
Re-exported so hotl_engine::LedgerSummary resolves alongside EngineEvent::LedgerReport — the loop-overhead instrument (§S1) lives in its own module ([ledger]) since it is self-contained (no dependency on the rest of the engine’s types) and independently unit-tested. The ten boundary events a turn’s sample passes through. Declaration order is the critical-path order the design doc’s per-phase deltas price (SnapshotReady−BoundaryStart prices the snapshot transport, etc.) — see [DELTA_PAIRS]. A doom-loop abort or a failed sample can legitimately skip phases past the point of failure; see [LoopLedger::stamp].
ProposeReply
What SessionCmd::ProposePrepared answers with — plain bool has no room for the rules-epoch guard’s distinction (commit-protocol.md §Proposal payloads): a stale epoch means “rebuild under the current rules and resend”, a genuinely different repair than “the log is sealed, stop trying”.
QuestionAnswer
Re-exported so hotl_engine::QuestionAnswer resolves alongside EngineEvent::Question — the type physically lives in hotl-types (shared with hotl-tools’s QuestionSink) to avoid a hotl-tools → hotl-engine dependency cycle; see question_sink’s doc comment. A human’s answer to an ask_user question (tier-1 gap #4). Lives here (not hotl-engine, where the plan first sketched it) so both hotl-tools’s QuestionSink and hotl-engine’s EngineEvent::Question can share one definition without either crate depending on the other — the same cycle-avoidance the plan flagged as open, resolved the way Question itself already is. NoHuman is the documented default when no reply arrives (headless, DontAsk, a dropped reply channel): the model must always get an answer, never a hang.
SampleStage
Whether the sample that produced a proposal had closed by the time the proposal was made — declared by the proposer, because only the turn knows. The actor never stores it as state and never routes on it; it exists so the held-steer release can check the argument it rests on instead of resting on a comment (commit-protocol.md §Read invariant, and the 72a6f1b held-steer rule).
SessionCmd
TurnEnd
How a turn task ended: with a user-facing outcome, or asking the actor to compact and respawn a continuation (M2 mid-turn = terminate → compact → respawn, per commit-protocol).

Traits§

Snapshotter
Workspace snapshots around mutating tool batches (M3b shadow-git). Implementations run the actual snapshot off-thread; a slow or absent snapshotter must never wedge the turn.

Functions§

event_channel
A fresh, not-yet-consumed event channel for a session that doesn’t exist yet — the events-side twin of session_channel. Split out so a caller can build a tool (ask_user) whose sink already holds a live sender to this session’s own events stream before the actor exists, the same chicken-and-egg session_channel solves for SessionCmd.
needs_continuation
Whether a projection ends on the model’s turn to speak (M4/#8): the last item is a user prompt or a batch of tool results the model never answered — i.e. an interrupted turn worth continuing on resume. A projection ending in an assistant item (or holding only instructions) is complete.
question_sink
The production hotl_tools::ask::QuestionSink for ask_user (tier-1 gap #4): mirrors Turn::ask almost line-for-line, but runs from inside a tool rather than Turn itself, so it reaches the actor through channels instead of self.propose/self.events directly. Durably commits PendingQuestion before surfacing (so a process that dies mid-question leaves a dangling record replay can warn about, exactly like PendingAsk), emits EngineEvent::Question carrying a fresh reply channel, races the human’s reply against the call’s own cancellation token (the same token Turn::ask races — an in-flight ask_user must never outlive a turn the user already cancelled), then commits QuestionResolved.
session_channel
A fresh, not-yet-consumed command channel for a session that doesn’t exist yet. Split out from spawn_session so a caller can build a tool (todo_write) whose sink already holds a live sender to this session’s actor before the actor exists — the registry (and the deps built from it) has to be assembled before spawn_session runs, which is otherwise a chicken-and-egg with a command channel spawn_session creates internally.
spawn_session
spawn_session_with
Spawn against a pre-created command channel (see session_channel); builds its own event channel.
spawn_session_with_channels
Spawn against pre-created command and event channels (see session_channel/event_channel) — what a caller needs when a session-scoped tool’s sink (ask_user) must hold live senders to both before the actor exists.