pub struct Task {Show 22 fields
pub schema: u32,
pub id: String,
pub title: String,
pub instruction: String,
pub repo: PathBuf,
pub source: Source,
pub priority: i32,
pub solo: bool,
pub status: TaskStatus,
pub attempts: usize,
pub runs: Vec<String>,
pub last_error: Option<String>,
pub hold_reason: Option<String>,
pub hold_source: Option<HoldSource>,
pub diagnostic: Option<String>,
pub blocked_by: Vec<String>,
pub block_reason: Option<String>,
pub answers: Vec<AnsweredQuestion>,
pub review_branch: Option<String>,
pub fresh_start: bool,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}Expand description
One unit of work.
Fields§
§schema: u32On-disk format version.
id: StringTask id, e.g. 20260902-140501-a1b2.
title: StringOne line, for lists and notifications.
instruction: StringThe task itself, handed to the graph verbatim.
repo: PathBufRepository to work in.
source: SourceWho asked.
priority: i32Higher runs first; ties break oldest-first so nothing starves.
solo: boolRun this task alone: one implementer, no panel of judges to convince.
#[serde(default)] so a queue file written before this field existed
still reads, as false - the ordinary multi-candidate competition,
unchanged. A task set to solo still runs the whole graph; only the
candidate count the daemon builds it with changes, and
crate::graph::Runner already collapses a single-candidate run to
implement → review → gate → merge on its own (see
crate::graph::Runner::review’s doc), so nothing about judging,
deliberation or voting had to change to support this.
status: TaskStatusCurrent state.
attempts: usizeHow many times this task has been claimed.
runs: Vec<String>Runs this task has produced, oldest first.
last_error: Option<String>Why the last attempt did not land.
hold_reason: Option<String>What a human hold is waiting on.
None covers both the ordinary cases: a hold the loop makes itself
(out of attempts, or the disk gate closed) explains itself through
Task::last_error instead, and a human hold nobody bothered to
explain is still a valid hold. The queue has no way to express a
dependency between two tasks, so on the occasions a hold really is
“wait for that other task first”, this is the only place that reason
survives - see Task::hold_manual and Task::release.
#[serde(default)] so a queue file written before this field existed
still reads, with no reason recorded rather than a parse error.
hold_source: Option<HoldSource>Who placed Task::hold_reason. None is a compatible old record;
see Task::operator_held for its deliberately conservative meaning.
diagnostic: Option<String>Diagnostic detail excerpted from the run that led to a hold - what a
human would have found opening artifacts/ by hand, not the one-line
reason in Task::last_error. Set only when a run’s own attempts are
exhausted and the task becomes TaskStatus::Held; daemon computes
it from the run’s own record, since this module has no notion of a
run’s internals. Bounded in length by the writer - see
daemon::diagnostic - so a verbose run cannot make this file grow
without limit.
#[serde(default)] so a queue file written before this field existed
still reads, with no diagnostic recorded rather than a parse error.
blocked_by: Vec<String>What this task is waiting on: other task ids, unanswered
crate::ask::Question ids, or both. Non-empty exactly when
TaskStatus::Blocked; emptying it — see Task::unblock — is what
puts the task back at TaskStatus::Queued.
Set by crate::conduct’s decisions and cleared deterministically by
crate::daemon as each dependency resolves, never by a person. Never
#[serde(default)] is skipped: a queue file from before this field
existed has nothing to report here, and an empty list is exactly that.
block_reason: Option<String>One line explaining the current Task::blocked_by, written by
crate::conduct. Cleared whenever blocked_by empties.
answers: Vec<AnsweredQuestion>Questions crate::conduct asked about this task that the operator has
since answered, oldest first — what was asked, and what they said.
A blocking question’s id leaves Task::blocked_by the moment
crate::ask::QuestionStatus::Answered is observed, but the id alone
tells nobody what was decided. This is what carries the answer’s
content forward: into the next conductor prompt for this task, and
into the instruction handed to the next run — see crate::daemon’s
deterministic blocker resolution. Kept for the task’s whole life, the
same as Task::runs: a release resets attempts, not evidence.
review_branch: Option<String>Set by crate::conduct when it chooses Review recovery for a task
whose branch survived a blocked run: the branch to reopen with
crate::graph::Runner::review instead of competing from scratch.
Requeues the task the same way Task::release does, so it is
picked up by the ordinary loop; crate::daemon reads this field once,
when it actually starts the run, and clears it either way — consumed
on success, dropped if the branch no longer exists by then. Never set
from the conductor’s own words: crate::daemon derives the branch
name itself from the task’s last run, so a hallucinated branch can
never reach here.
fresh_start: boolA release deliberately starts a new competition instead of resuming
the prior run. History remains as evidence in runs.
created_at: TimestampWhen the task was filed.
updated_at: TimestampLast change to this file.
Implementations§
Source§impl Task
impl Task
Sourcepub fn new(
title: String,
instruction: String,
repo: PathBuf,
source: Source,
) -> Self
pub fn new( title: String, instruction: String, repo: PathBuf, source: Source, ) -> Self
File a new task. Persist it with Queue::put.
Sourcepub fn succeed(&mut self)
pub fn succeed(&mut self)
Record a successful run.
Both magi task done and POST /api/queue/{id}/done can close a held
task directly, with no release in between, so this clears
hold_reason the same way Task::release does. Otherwise a task
held for “waiting on 3ed9” and then closed as done without ever being
released would still read as waiting on something in magi task show
and on its card, after it no longer is.
Sourcepub fn fail(&mut self, why: impl Into<String>, max_attempts: usize)
pub fn fail(&mut self, why: impl Into<String>, max_attempts: usize)
Record a failed attempt. Out of attempts means held for a human, rather than retried until the money runs out.
Clears Task::diagnostic unconditionally: it belongs to whatever run
produced it, and a caller that has one for this attempt sets it
itself right after calling this, once it knows the task actually ended
up TaskStatus::Held - see daemon::diagnostic. Without the clear, a
task released after a diagnosed hold and then failed again for an
unrelated, undiagnosed reason (a config error, say) would go on
showing the previous run’s diagnostic as if it explained the new one.
Sourcepub fn stall(&mut self, why: impl Into<String>)
pub fn stall(&mut self, why: impl Into<String>)
Record an attempt that failed for a reason the task is not responsible for - the agent CLIs ran out of quota and the judging panel collapsed.
This refunds the attempt on purpose. A quota window closing at 4am must
not spend the backlog’s retry budget: the operator would come back to a
queue of held tasks that were never actually judged, and would have to
release every one by hand to find out which had a real problem. The task
goes back to Failed, which the loop retries, so a reset quota picks the
work up where it stopped.
Sourcepub fn operator_held(&self) -> bool
pub fn operator_held(&self) -> bool
Whether this held task may only be released by an operator.
Old files did not record a source. Preserve every such hold rather
than guessing that it was automatic and risking duplicate work. New
automatic holds record HoldSource::Machine and remain recoverable.
Sourcepub fn hold_manual(&mut self, reason: Option<String>)
pub fn hold_manual(&mut self, reason: Option<String>)
Take this task out of the loop’s reach by an operator action.
Sourcepub fn hold_machine(&mut self, reason: Option<String>)
pub fn hold_machine(&mut self, reason: Option<String>)
Take this task out of the loop’s reach during automatic recovery.
Sourcepub fn block(&mut self, blocked_by: Vec<String>, reason: Option<String>)
pub fn block(&mut self, blocked_by: Vec<String>, reason: Option<String>)
Block this task on other task ids and/or open question ids, chosen by
crate::conduct. Pure: the caller still owns writing it back with
Queue::put.
Sourcepub fn unblock(&mut self, resolved_id: &str)
pub fn unblock(&mut self, resolved_id: &str)
Remove one resolved dependency (a task id that became TaskStatus::Done,
or a question id that became crate::ask::QuestionStatus::Answered).
Once nothing is left in Task::blocked_by, the task returns to
TaskStatus::Queued on its own - deciding why a task was blocked
was crate::conduct’s job, but noticing a dependency resolved needs no
model at all.
A no-op, on purpose, for a task that is not TaskStatus::Blocked:
crate::daemon’s deterministic resolver runs over every task on every
poll, and a task that moved on for some other reason must not be
dragged back to Queued by a stale id it still happens to carry.
Sourcepub fn record_answer(&mut self, question: String, answer: String)
pub fn record_answer(&mut self, question: String, answer: String)
Record that a question crate::conduct asked about this task has been
answered, so the answer’s content — not just the fact that the
question is gone — reaches the next conductor prompt and the next
run’s instruction. See Task::answers.
Sourcepub fn request_review(&mut self, branch: String)
pub fn request_review(&mut self, branch: String)
Requeue this task to reopen its last run as a review-only pass against
branch (crate::graph::Runner::review) rather than competing from
scratch. See Task::review_branch.
Sourcepub fn requeue(&mut self)
pub fn requeue(&mut self)
Requeue after a conductor chose a new competition. Unlike an ordinary operator release, this deliberately does not resume the old run.
Sourcepub fn set_priority(&mut self, priority: i32) -> Result<()>
pub fn set_priority(&mut self, priority: i32) -> Result<()>
Change how urgently this task should run next.
Refused once the task is running: priority only feeds the sort
Queue::next_runnable does over tasks waiting to be claimed, and a
running task has already left that pool. Accepting the write anyway
would look like it worked while changing nothing until - and unless -
this attempt fails and the task becomes runnable again, which is a
surprise the phone should not hand back as a success.
Sourcepub fn edit(&mut self, title: String, instruction: String) -> Result<()>
pub fn edit(&mut self, title: String, instruction: String) -> Result<()>
Replace this task’s title and instruction wholesale.
Restricted to queued and held. A running task’s instruction has
already been handed to the graph, so a run in flight and the file on
disk must not be allowed to disagree about what was asked; a done or
failed task is a record of what actually happened and editing it
after the fact would falsify that record. id, created_at,
source, and runs are left untouched on purpose - an edit stands in
for “delete and refile”, and keeping the id, the timestamp, the
attribution, and the run history is the entire reason it exists
instead.
Sourcepub fn handed_off(&mut self, why: impl Into<String>)
pub fn handed_off(&mut self, why: impl Into<String>)
Record a run that produced a pull request without merging it.
The task is held rather than retried, and it costs no further attempt either way. The work the task asked for exists: it is sitting on a branch, in a pull request, waiting for CI or for a person. Retrying would spend the whole competition budget a second time and then race a second branch against the pull request the first one opened - which is exactly what happened to run 01c2, whose finished and green pull request was re-competed from scratch four seconds after it opened.
A pull request nobody merged is a request for a person, not a failure.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Task
impl<'de> Deserialize<'de> for Task
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>,
Auto Trait Implementations§
impl Freeze for Task
impl RefUnwindSafe for Task
impl Send for Task
impl Sync for Task
impl Unpin for Task
impl UnsafeUnpin for Task
impl UnwindSafe for Task
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
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