pub struct Questions { /* private fields */ }Expand description
A question store on disk.
Implementations§
Source§impl Questions
impl Questions
Sourcepub fn at(root: PathBuf) -> Self
pub fn at(root: PathBuf) -> Self
A store at an explicit root. Tests use this, which is why none of them need the operator’s real home.
Sourcepub fn panel_dir(&self, id: &str) -> PathBuf
pub fn panel_dir(&self, id: &str) -> PathBuf
Directory holding one question’s panel, <root>/<id>.panel.
Sourcepub fn put_panel(
&self,
q: &mut Question,
html: &str,
assets: &[PathBuf],
) -> Result<()>
pub fn put_panel( &self, q: &mut Question, html: &str, assets: &[PathBuf], ) -> Result<()>
Store a panel: the html, plus assets copied in under their base
names. Updates q.panel and q.assets; the caller then puts the
question, or the record on disk will deny having a panel that exists.
The assets are copied, not referenced. An agent authors its panel
inside a candidate worktree and points at files there, and magi fold
deletes those worktrees; a question is the permanent record of a
decision the owner took, so a panel that referenced its own images
would render as broken boxes exactly when someone went back to ask why
the decision was made. Copying follows symlinks - std::fs::copy
does, and so does the std::fs::metadata the size is measured with,
so the bytes counted and the bytes written are the same target file’s -
which is the intent: storing a link would leave the panel pointing at
the worktree again, one indirection further away.
Everything that can be rejected is rejected before the first byte is written, and the panel is then assembled in a scratch directory and swapped in. So a refusal leaves the previous panel intact, and a success replaces it wholesale rather than merging: a re-asked question showing one attempt’s diff next to another attempt’s table would be a panel neither agent ever wrote.
Sourcepub fn panel_html(&self, id: &str) -> Option<String>
pub fn panel_html(&self, id: &str) -> Option<String>
The panel’s html, or None when the question has no panel.
None rather than an error for a missing panel because the caller is a
web handler whose answer is 404 either way, and an unreadable panel is
not a reason to fail the question it belongs to.
Sourcepub fn panel_asset(&self, id: &str, name: &str) -> Result<Option<Vec<u8>>>
pub fn panel_asset(&self, id: &str, name: &str) -> Result<Option<Vec<u8>>>
One file from a panel. Ok(None) is “no such file”; Err is “that is
not a name a panel file can have”.
Rejects a name failing valid_asset_name before touching the
filesystem, which is the whole point of the second check: the name
arrives from a URL, the directory is on disk where any process could
have dropped a file, and <root>/<id>.panel/../../id_rsa is a path the
operating system would resolve perfectly happily. The two callers’
distinct outcomes - 400 for a name, 404 for a file - are why this is
Result<Option<_>> rather than one flattened Option.
Sourcepub fn drop_panel(&self, id: &str) -> Result<()>
pub fn drop_panel(&self, id: &str) -> Result<()>
Delete a question’s panel, and any scratch a killed put_panel left.
Succeeds when there is nothing to delete, so a caller cleaning up does
not have to know whether a panel was ever written. The question record
is not touched: the caller clears panel and assets and puts it,
in the same order as everywhere else here.
Sourcepub fn put(&self, q: &mut Question) -> Result<()>
pub fn put(&self, q: &mut Question) -> Result<()>
Write a question, atomically, so a process killed mid-write leaves the previous state readable rather than a truncated file that would strand the run waiting on it.
Sourcepub fn get(&self, id: &str) -> Result<Question>
pub fn get(&self, id: &str) -> Result<Question>
Load a question by id or unambiguous id prefix.
Sourcepub fn list(&self) -> Vec<Question>
pub fn list(&self) -> Vec<Question>
Every question on disk: open first, then newest first.
Open first because that ordering is the product - the list exists to show the operator what has stopped, and an answered question is history underneath it. Unreadable files are skipped rather than fatal: one corrupt question must not take the web UI down, and must certainly not hide the open question the operator was looking for.
Sourcepub fn open_for(&self, run: &str) -> Vec<Question>
pub fn open_for(&self, run: &str) -> Vec<Question>
Open questions belonging to one run, newest first.
Used to decide whether a parked run can be resumed: while this is non-empty, nothing about the run has changed and no agent should be spawned for it.
Sourcepub fn abandon_for_run(&self, run: &str, why: &str) -> Result<usize>
pub fn abandon_for_run(&self, run: &str, why: &str) -> Result<usize>
Abandon every open question belonging to a run, and report how many.
Called when a run’s record is deleted. The agent that asked died with the run, so there is nobody left to hand an answer to, and a question left open would keep asking the operator for a decision that can no longer be delivered - the phone showed exactly that: “auth.rs というファ イルが見つかりません” with two buttons, for a run whose directory had been gone for two hours.
Abandoned rather than deleted, because Question::abandon already
means “this can no longer be answered” and the record of having asked
is worth keeping. Answered questions are left exactly as they are.
Sourcepub fn settle_run(&self, run: &str, status: RunStatus) -> Result<usize>
pub fn settle_run(&self, run: &str, status: RunStatus) -> Result<usize>
Abandon a run’s open questions once status says the run is not
coming back, worded with what it actually became.
The run-deleted case above and this one are the same fact - nobody is
left to read an answer - reached by two different doors. This is the
one for a run that finished on its own: merged, reached Ready with
nothing left to do, or failed outright with no established point to
resume from. Those are exactly the statuses RunStatus::resumable
excludes, and that is the line this draws too - deliberately not
RunStatus::done, which also counts Blocked and Stalled as
over. Both of those can still be picked back up with the candidates,
the review round and the seat sessions already on disk, so a question
asked mid-round may yet get a real answer from a real resume, and
folding it here would be exactly the mistake this function exists to
avoid on the other side - answering back into a run that no longer
exists to read it.
A no-op, not an error, when status is still resumable or when there
was nothing open to begin with - callers reach this from more than one
place a run can settle, and a second call finding nothing left to
abandon is the expected case, not a bug.
Sourcepub fn resolve_id(&self, prefix: &str) -> Result<String>
pub fn resolve_id(&self, prefix: &str) -> Result<String>
Expand an id prefix to exactly one question id. The short id the phone and the reports show is a suffix, so that is accepted too.
Sourcepub fn revision(&self) -> u64
pub fn revision(&self) -> u64
Newest modification time in the store, in milliseconds, for change
detection. The web UI compares this instead of re-reading every
question, so an idle phone on a slow link costs one stat per file.
Sourcepub fn count_open(&self) -> usize
pub fn count_open(&self) -> usize
How many questions are open, whichever side of the conversation is
holding the ball right now. Ten turns of back and forth between the
owner and the agent are still one open question - see
Question::say - so this does not drop while a reply is in
flight. Self::count_needs_owner is the number that does.
Sourcepub fn count_needs_owner(&self) -> usize
pub fn count_needs_owner(&self) -> usize
How many open questions actually need the owner right now: open, and
not Question::waiting_on_agent.
This is the number a notification channel owes - the ask bar, the nav
badge, the document title - because those exist to say “something
needs you”, and a question sitting in magi ask --thread limbo does
not. count_open stays as it is for Self::open_for’s callers,
where a round trip must not look like the run resumed.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Questions
impl RefUnwindSafe for Questions
impl Send for Questions
impl Sync for Questions
impl Unpin for Questions
impl UnsafeUnpin for Questions
impl UnwindSafe for Questions
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,
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