pub struct RunRecord {
pub id: RunId,
pub task_id: TaskId,
pub status: RunStatus,
pub step_entries: Vec<StepEntry>,
pub degradations: Vec<DegradationEntry>,
pub operator_sid: Option<String>,
pub current: BTreeMap<String, Assignee>,
pub next_generation: u64,
pub result_ref: Option<Value>,
pub input_json: Option<String>,
pub created_at: u64,
pub updated_at: u64,
}Expand description
One persisted Run row — one kick of a crate::store::task::TaskRecord.
Fields§
§id: RunIdRun identifier.
task_id: TaskIdThe Task this Run was kicked from.
status: RunStatusCurrent lifecycle status.
step_entries: Vec<StepEntry>Trace of dispatched steps, in append order.
degradations: Vec<DegradationEntry>Worker-reported degradations, in append order (GH #32). Independent
channel from Self::step_entries/Self::result_ref — see
DegradationEntry’s doc for the invariant. [] (the default) =
no degradations reported — every pre-#32 Run is unaffected.
operator_sid: Option<String>Operator session id bound to this Run, if any (WS operator correlation).
This is the launch-time snapshot of who was pinned when the Run
was kicked — not the live holder. The live holder is
Self::current; nothing resolves a dispatch destination from this
field, so the two are not competing destinations (A10).
current: BTreeMap<String, Assignee>The Run’s live holders, keyed by slot — the model’s
Run.current (§4.3).
A slot is a Blueprint-declared Operator seat (operator_ref =
Blueprint.operators[].name): a Blueprint may declare several, and
each agent picks the one it dispatches through. The cardinality is
therefore Run 1 : N Operator and Operator 1 : 1 Assignee (at a
time), hence Run 1 : N Assignee — with A1 reading “at most one
holder per slot”, which is exactly what a map expresses. An
absent key is that slot’s Vacant; an empty map is a Run with no
slot held at all.
R2 — a Vacant slot does not stop the Run; only a dispatch that
needs that slot’s holder is affected, and dispatches through other
slots are untouched. R6 — this travels with the Run row, so a
restart does not drop the assignments.
Only RunStore::acquire_assignee / RunStore::vacate_assignee
write it, both scoped to one slot, and both mint a fresh
Assignee rather than mutating a stored one (Q3).
BTreeMap rather than HashMap: the
map is serialized into a persisted column and into observation
payloads, and key-sorted output keeps those bytes stable across
processes. Additive with #[serde(default)] so rows serialized
before the assignment axis existed decode unchanged (as an empty
map = every slot Vacant).
§An empty map is written out, not skipped
This field used to carry skip_serializing_if = "BTreeMap::is_empty", so a Run holding nothing had no current
key on the wire at all. That made “nobody holds anything on this
Run” and “this response does not report holders” the same bytes,
and §4.3 asks for the opposite (居なければ居ないと分かる — when
nobody is there, it must be possible to tell that nobody is there).
"current": {} says it. The per-seat form of the same answer, which
also names the seats nobody holds, is
[crate::handover::run_assignees].
next_generation: u64The Run’s generation counter — the model’s G (A4).
0 at launch. Every assignment event (Assign or Vacant)
increments it by one before stamping, so the first Assign
yields gen == 1; the counter therefore holds the generation of
the most recent event, and the next event will use this value + 1.
The bump is unconditional — re-acquiring for the SAME op still
increments, because the counter counts events, not state changes.
One counter per Run, shared by every slot. An Assign to slot
b advances the same G that a preceding Assign to slot a
advanced, so any two holders — of the same slot or of different
ones — can be ordered by gen. Per-slot counters would buy nothing
and would make that comparison meaningless.
A2 (current = Assigned(a) ⟹ a.gen ≤ G) holds on two legs, not
one. On the write path it holds by construction: every current
value’s gen is stamped from this counter at the moment it is
bumped, so an acquire can never leave a holder above it. On the way
in it is checked — RunStore::create takes a caller-supplied
record with both fields public, so a record that arrives already
violating A2 is refused with
RunStoreError::AssigneeGenerationAhead rather than stored (see
RunRecord::validate_assignment_generations). Left unchecked, that
record would stay violated: the next acquire stamps generation 1,
below the seeded incumbent, and ordering two holders by gen — the
whole reason G is Run-wide — would silently invert.
Additive with #[serde(default)] (pre-existing rows read back 0).
result_ref: Option<Value>The Run’s terminal result payload, set once by
RunStore::set_result. None while the Run is in flight.
input_json: Option<String>Opaque JSON snapshot of the launch input this Run was kicked with
(blueprint / init_ctx / operator injection / ttl / …). The server
serializes its own launch-input struct into this string at Run
creation time so an Interrupted Run can be resumed under the SAME
run_id without re-deriving the input from a since-stale request
body. The store treats it as an opaque blob — the schema is owned by
the caller (the server crate). None = no snapshot recorded (older
rows predating resume support, or a caller that never opts in); such
a Run cannot be resumed. Additive with #[serde(default)] so
pre-existing serialized rows deserialize unchanged.
created_at: u64Unix epoch seconds — creation time.
updated_at: u64Unix epoch seconds — last update time.
Implementations§
Source§impl RunRecord
impl RunRecord
Sourcepub fn validate_assignment_generations(&self) -> Result<(), RunStoreError>
pub fn validate_assignment_generations(&self) -> Result<(), RunStoreError>
A2 as a check: every holder in Self::current must have been
stamped at or below Self::next_generation
(current = Assigned(a) ⟹ a.gen ≤ G).
RunStore::create calls this on the record it is handed, and every
RunStore implementation is expected to — an out-of-tree backend
that skips it accepts records the two in-tree backends refuse.
Nothing else needs it: acquire_assignee stamps gen from the
counter it has just bumped, so no write path in this crate can
produce a record this rejects.
Reports the first offending seat in Self::current’s key order,
which is stable (BTreeMap), so the same bad record always names the
same seat.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for RunRecord
impl<'de> Deserialize<'de> for RunRecord
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl JsonSchema for RunRecord
impl JsonSchema for RunRecord
Source§fn schema_id() -> Cow<'static, str>
fn schema_id() -> Cow<'static, str>
Source§fn json_schema(generator: &mut SchemaGenerator) -> Schema
fn json_schema(generator: &mut SchemaGenerator) -> Schema
Source§fn inline_schema() -> bool
fn inline_schema() -> bool
$ref keyword. Read moreAuto Trait Implementations§
impl Freeze for RunRecord
impl RefUnwindSafe for RunRecord
impl Send for RunRecord
impl Sync for RunRecord
impl Unpin for RunRecord
impl UnsafeUnpin for RunRecord
impl UnwindSafe for RunRecord
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more