Expand description
Held-task triage: walking held tasks so a hold left by an accident does
not sit unread forever next to one a human placed on purpose.
crate::queue::HoldSource already distinguishes “the daemon or
conductor held this during its own recovery” (HoldSource::Machine,
documented as recoverable) from “an operator held this on purpose”
(HoldSource::Manual). What was missing was anything that actually acts
on that distinction: nothing walked the held list and asked whether a
machine hold’s cause was still true, and a record written before
hold_source existed (schema < 3, None) was silently protected forever
by Task::operator_held’s conservative default - never wrong, but also
never looked at again by anything.
run_once is that walk. For every held task it finds:
HoldSource::Machine: if [machine_cause_resolved] can tell the cause is gone, the task goes straight back toqueued- the same effect asmagi task release, just automatic. When it cannot tell, aQuestionis filed once and the task stays held until answered.None(a legacy record, or a hold nobody explained): always a question, exactly once - the whole point being that “protected forever” must not mean “never shown to anyone” either.HoldSource::Manual: never touched automatically. Only once the hold has sat untouched past [MANUAL_STALE_AFTER] does it earn a question of its own, asking whether it is still wanted.
Every question this module files carries NODE and uses the task id as
Question::run - the same convention crate::conduct’s own questions
use for a task rather than a run (see conduct::apply_one’s own comment
on why the dedupe check there also filters on node, not run alone: an
ordinary graph question’s run is a real run id, and a coincidental
equality with some task’s id must not be read as “about this task”).
[latest_triage_question] follows the identical rule.
§Why answers apply here rather than through crate::queue::Task::block
crate::conduct blocks a task on its own question
(Task::block(vec![question_id], …)), and crate::daemon::resolve_blockers
unblocks it - unconditionally, back to queued - the moment that question
is answered, whatever the answer actually said. That is correct for
conduct: the content of the answer is meant for whoever reads
Task::answers next, not for the resolver.
A triage question’s answer is different: “not yet” and “discard it” do two
entirely different, non-resuming things, and only “resume it” may put the
task back in line. Reusing the generic blocked/unblock path would resume
every answer alike, so this module never calls Task::block and never
leaves a triaged task anything but held while its question is open.
[interpret_answer] reads Question::resolution itself and
run_once acts on it directly: Task::release for an actual “resume
it” choice, Queue::remove for “discard it” (捨ててよい really means
“you may throw this away”, not “leave it sitting held” - the English
wording must say the same thing, not “leave it held”), and
Task::hold_manual for anything else - which both keeps the task held
and reclassifies it as a hold an operator has now actually seen, one
crate::conduct and a later triage pass leave alone.
The choice is read by its position in Question::choices
([Wording::choices3]/[Wording::choices2] always put “resume” first and
“discard” third), never by comparing the answer text against [Wording]’s
own strings picked from whatever config is in force now - the language a
question was filed in and the language a later run_once call happens to
read back are not guaranteed to be the same call’s Config, and a text
comparison would silently misread a real “resume” answer as “keep held”
the moment they disagree.
§Idempotency, without a new field
run_once runs on every daemon idle tick (see crate::daemon::poll)
and on every magi task triage, so applying the same answered question
twice has to be harmless - and, once a HoldSource::Manual/None
question has been answered “not yet”, finding a fresh one for the same
task later (once it goes stale again) has to still be possible. Neither
crate::queue::Task nor crate::ask::Question has a field for “this
answer was already applied”, so [already_applied] reads the same
Question::short id back out of Task::hold_reason that
[keep_held_note] appended to it - the same trick Question::abandon
already uses to fold a fact into a text field that has no dedicated one.
Appended, not written wholesale: the reason the hold happened in the first
place is still worth reading in magi task show after an operator says
“not yet”. A “resume” or “discard” answer needs no marker at all: the task
either leaves held entirely or stops existing, and either way it is
never looked at by this module again.
Structs§
Constants§
- NODE
- Node recorded on every question this module files -
crate::conduct::NODEfor the same idea applied to acrate::conductdecision instead.
Functions§
- open_
question_ for - The open triage question about
task_id, if any - whatmagi task showprints so a held task’s card names the question waiting on it, not only its hold reason.Noneonce it is answered or abandoned: nothing is waiting on it anymore. - open_
task_ ids - Every task id with an open triage question right now - what
magi task listuses to mark a held task that is already waiting on an operator decision, rather than have it read identically to one nobody has looked at yet. - run_
once - Run one deterministic triage pass over every
heldtask inqueue. No model call anywhere in this function - see this module’s own doc for what eachHoldSourcegets instead.