Skip to main content

artifactize_core/store/
requests.rs

1use std::{collections::HashMap, path::Path, sync::Arc};
2
3use serde::Serialize;
4
5use rusqlite::OptionalExtension;
6use tokio_rusqlite::Connection;
7
8use super::{
9    DATABASE, Execution, HumanClaim, Request,
10    definitions::Graph,
11    receipts::{Error, check_files, schema_initialized},
12};
13use crate::workspace::{canonical_target, outside_workspace};
14
15/// A request with what it shows. Its decoded records are shared: a copy of the view, and the
16/// Run's view of the same read, keep the one request and execution, so a snapshot that is
17/// read again copies only the requests that changed.
18#[derive(Debug, Clone, Serialize)]
19pub struct RequestView {
20    #[serde(flatten)]
21    pub request: Arc<Request>,
22    pub claim: Option<HumanClaim>,
23    pub execution: Option<Arc<Execution>>,
24    pub definition: Option<RequestDefinition>,
25}
26
27/// The saved definitions of a request's eval and Artifact.
28#[derive(Clone)]
29pub struct RequestDefinition(Place);
30#[derive(Clone)]
31enum Place {
32    /// A place in the Run's one decoded graph, which every view of an opened Run shares.
33    Graph {
34        graph: Arc<Graph>,
35        /// The eval's position in the graph's evals.
36        eval: usize,
37        artifact: crate::types::ArtifactName,
38    },
39    /// Copies of its own eval and Artifact, for a view read apart from its Run, such as a
40    /// waiting review's, which may be kept long after the rest of the Run is gone.
41    Own {
42        eval: Arc<super::definitions::Eval>,
43        artifact: Arc<super::definitions::Artifact>,
44    },
45}
46impl RequestDefinition {
47    pub fn eval(&self) -> &super::definitions::Eval {
48        match &self.0 {
49            Place::Graph { graph, eval, .. } => &graph.evals()[*eval],
50            Place::Own { eval, .. } => eval,
51        }
52    }
53    pub fn artifact(&self) -> &super::definitions::Artifact {
54        match &self.0 {
55            Place::Graph {
56                graph, artifact, ..
57            } => graph
58                .artifact(artifact)
59                .expect("an indexed request's Artifact is in its graph"),
60            Place::Own { artifact, .. } => artifact,
61        }
62    }
63}
64impl Serialize for RequestDefinition {
65    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
66        use serde::ser::SerializeStruct;
67        let mut definition = serializer.serialize_struct("RequestDefinition", 2)?;
68        definition.serialize_field("eval", self.eval())?;
69        definition.serialize_field("artifact", self.artifact())?;
70        definition.end()
71    }
72}
73impl std::fmt::Debug for RequestDefinition {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("RequestDefinition")
76            .field("eval", self.eval())
77            .field("artifact", self.artifact())
78            .finish()
79    }
80}
81
82#[derive(Default)]
83struct Filter<'a> {
84    run: Option<&'a str>,
85    id: Option<&'a str>,
86    repo: Option<&'a Path>,
87    waiting: bool,
88    session: Option<&'a str>,
89    statuses: &'a [crate::types::RequestStatus],
90    /// A bounded slice of the ordered requests: offset and count.
91    slice: Option<(u64, u32)>,
92    shown: Shown,
93}
94
95/// What the views of a read show besides their requests.
96#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
97pub enum Shown {
98    /// The definitions their Runs saved, which hold the Runs' whole graphs: each Run read is
99    /// decoded whole.
100    #[default]
101    Definitions,
102    /// No definitions: only the Runs' repositories are read, never their records.
103    Requests,
104}
105
106pub async fn read_requests(state: &Path, run: Option<&str>) -> Result<Vec<RequestView>, String> {
107    read(
108        state,
109        Filter {
110            run,
111            ..Filter::default()
112        },
113    )
114    .await
115}
116
117pub async fn read_requests_in(
118    state: &Path,
119    run: Option<&str>,
120    repo: Option<&Path>,
121) -> Result<Vec<RequestView>, String> {
122    read(
123        state,
124        Filter {
125            run,
126            repo,
127            ..Filter::default()
128        },
129    )
130    .await
131}
132
133pub async fn read_request(state: &Path, id: &str) -> Result<RequestView, String> {
134    read_request_as(state, id, Shown::Definitions).await
135}
136
137/// Request `id` with what `shown` names besides it.
138pub async fn read_request_as(state: &Path, id: &str, shown: Shown) -> Result<RequestView, String> {
139    read(
140        state,
141        Filter {
142            id: Some(id),
143            shown,
144            ..Filter::default()
145        },
146    )
147    .await?
148    .pop()
149    .ok_or_else(|| "Review request not found.".into())
150}
151
152/// The request whose Agent review ran as session `id`, if this state has it.
153pub async fn read_session_request(state: &Path, id: &str) -> Result<Option<RequestView>, String> {
154    Ok(read(
155        state,
156        Filter {
157            session: Some(id),
158            ..Filter::default()
159        },
160    )
161    .await?
162    .pop())
163}
164
165/// The request that owns `view`'s shared execution: a follower of another Run joins the
166/// original waiting request, which alone records the Human definition (tools and owner
167/// schemas), the claim and the result. `view` itself when it is the original.
168pub async fn read_original(state: &Path, view: &RequestView) -> Result<RequestView, String> {
169    read_original_as(state, view, Shown::Definitions).await
170}
171
172/// The request that owns `view`'s shared execution, read with what `shown` names besides it.
173pub async fn read_original_as(
174    state: &Path,
175    view: &RequestView,
176    shown: Shown,
177) -> Result<RequestView, String> {
178    let original = view
179        .execution
180        .as_ref()
181        .map(|execution| &execution.provenance.request_id)
182        .or_else(|| {
183            view.request
184                .provenance
185                .as_ref()
186                .map(|source| &source.request_id)
187        });
188    match original.filter(|id| *id != &view.request.id) {
189        Some(id) => read_request_as(state, id, shown).await,
190        None => Ok(view.clone()),
191    }
192}
193
194/// One entry per waiting Human sign-off: followers collapse onto the original request that
195/// owns their shared execution, at the position of the first entry that names it.
196pub async fn read_waiting_originals(
197    state: &Path,
198    repo: Option<&Path>,
199) -> Result<Vec<RequestView>, String> {
200    let waiting = read_waiting(state, repo).await?;
201    let by_id: std::collections::BTreeMap<_, _> = waiting
202        .iter()
203        .map(|view| (&view.request.id, view))
204        .collect();
205    let mut seen = std::collections::BTreeSet::new();
206    let mut originals: Vec<RequestView> = Vec::with_capacity(waiting.len());
207    for view in &waiting {
208        let source = view
209            .execution
210            .as_ref()
211            .map_or(&view.request.id, |execution| {
212                &execution.provenance.request_id
213            });
214        if seen.contains(source) {
215            continue;
216        }
217        let original = match by_id.get(source) {
218            Some(original) => Some((*original).clone()),
219            None => read_original(state, view).await.ok(),
220        };
221        // A follower whose original is gone or no longer waits stays listed as itself.
222        let original = original
223            .filter(|original| original.request.status().is_human_wait())
224            .unwrap_or_else(|| view.clone());
225        if seen.insert(original.request.id.clone()) {
226            originals.push(original);
227        }
228    }
229    Ok(originals)
230}
231
232/// Up to `limit` requests with one of `statuses` (any status when empty) from position
233/// `offset` of every Run's requests, newest Run first, each with its Run's repository and what
234/// `shown` names besides it.
235pub async fn read_request_batch(
236    state: &Path,
237    statuses: &[crate::types::RequestStatus],
238    offset: u64,
239    limit: u32,
240    shown: Shown,
241) -> Result<Vec<(RequestView, std::path::PathBuf)>, String> {
242    read_with_repo(
243        state,
244        Filter {
245            statuses,
246            slice: Some((offset, limit)),
247            shown,
248            ..Filter::default()
249        },
250    )
251    .await
252}
253
254/// WAITING_HUMAN requests, newest Run first, from the canonical `repo`'s Runs or every Run.
255/// Their views have no definitions: what waits, not what it reviews, so the read does not grow
256/// with the waiting Runs' workspaces.
257pub async fn read_waiting(state: &Path, repo: Option<&Path>) -> Result<Vec<RequestView>, String> {
258    read(
259        state,
260        Filter {
261            repo,
262            waiting: true,
263            shown: Shown::Requests,
264            ..Filter::default()
265        },
266    )
267    .await
268}
269
270async fn read(state: &Path, filter: Filter<'_>) -> Result<Vec<RequestView>, String> {
271    Ok(read_with_repo(state, filter)
272        .await?
273        .into_iter()
274        .map(|(view, _)| view)
275        .collect())
276}
277
278/// The repository of the Run that request `id` belongs to, where its Human actions are
279/// recorded. Only the request's Run row is read, never the Run's record or definitions, so a
280/// Human action finds its receipts at once however large the Run's workspace is.
281pub async fn read_request_repo(state: &Path, id: &str) -> Result<std::path::PathBuf, String> {
282    let not_found = || "Review request not found.".to_owned();
283    let id: crate::types::RequestId = id.parse()?;
284    let state = canonical_target(state).map_err(|e| e.to_string())?;
285    let Some(connection) = open_read(&state).await? else {
286        return Err(not_found());
287    };
288    let result = connection
289        .call(move |db| -> Result<_, Error> {
290            super::wait_when_busy(db)?;
291            let transaction = db.transaction()?;
292            if !schema_initialized(&transaction)? {
293                return Ok(None);
294            }
295            let repo = transaction
296                .query_row(
297                    "SELECT r.repo FROM requests q JOIN runs r ON r.id=q.run_id WHERE q.id=?",
298                    [&id],
299                    |row| row.get::<_, String>(0),
300                )
301                .optional()?
302                .map(std::path::PathBuf::from);
303            transaction.commit()?;
304            if let Some(repo) = &repo {
305                crate::workspace::outside_workspaces(std::iter::once(repo.as_path()), &state)
306                    .map_err(|error| Error::Invalid(error.to_string()))?;
307            }
308            Ok(repo)
309        })
310        .await
311        .map_err(|e| e.to_string());
312    super::connection::finish(connection, result)
313        .await?
314        .ok_or_else(not_found)
315}
316
317/// A read-only connection to the database of the canonical `state`, or none before it has one.
318pub(super) async fn open_read(state: &Path) -> Result<Option<Connection>, String> {
319    check_files(state)?;
320    if !state
321        .join(DATABASE)
322        .try_exists()
323        .map_err(|e| e.to_string())?
324    {
325        return Ok(None);
326    }
327    Connection::open_with_flags(
328        state.join(DATABASE),
329        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
330    )
331    .await
332    .map(Some)
333    .map_err(|e| e.to_string())
334}
335
336async fn read_with_repo(
337    state: &Path,
338    filter: Filter<'_>,
339) -> Result<Vec<(RequestView, std::path::PathBuf)>, String> {
340    let state = canonical_target(state).map_err(|e| e.to_string())?;
341    let repo = filter
342        .repo
343        .map(canonical_target)
344        .transpose()
345        .map_err(|e| e.to_string())?;
346    if let Some(repo) = &repo {
347        outside_workspace(repo, &state).map_err(|e| e.to_string())?;
348    }
349    let run = filter
350        .run
351        .map(str::parse::<crate::types::RunId>)
352        .transpose()?;
353    let id = filter
354        .id
355        .map(str::parse::<crate::types::RequestId>)
356        .transpose()?;
357    let repo = repo.map(|repo| crate::platform::path_text(&repo));
358    let waiting = filter.waiting;
359    let statuses = filter.statuses.to_vec();
360    let slice = filter.slice;
361    let session = filter
362        .session
363        .map(str::parse::<crate::types::SessionId>)
364        .transpose()?;
365    let shown = filter.shown;
366    let Some(connection) = open_read(&state).await? else {
367        return Ok(Vec::new());
368    };
369    let result = connection
370        .call(move |db| -> Result<_, Error> {
371            super::wait_when_busy(db)?;
372            let transaction = db.transaction()?;
373            if !schema_initialized(&transaction)? {
374                return Ok(Vec::new());
375            }
376            let filter = Query {
377                run,
378                id,
379                repo,
380                waiting,
381                session,
382                statuses,
383                slice,
384                ids: None,
385            };
386            let rows = rows(&transaction, &filter)?;
387            let ids: std::collections::BTreeSet<_> = rows.iter().map(|row| &row.run).collect();
388            let mut statement = transaction.prepare(match shown {
389                Shown::Definitions => "SELECT r.id,r.repo,r.data,d.data FROM runs r LEFT JOIN run_definitions d ON d.run_id=r.id WHERE r.id IN (SELECT value FROM json_each(?))",
390                Shown::Requests => "SELECT r.id,r.repo,NULL,NULL FROM runs r WHERE r.id IN (SELECT value FROM json_each(?))",
391            })?;
392            let runs = statement
393                .query_map([serde_json::to_string(&ids)?], |row| {
394                    Ok((
395                        row.get::<_, crate::types::RunId>(0)?,
396                        row.get::<_, String>(1)?,
397                        row.get::<_, Option<String>>(2)?,
398                        row.get::<_, Option<String>>(3)?,
399                    ))
400                })?
401                .collect::<Result<Vec<_>, _>>()?;
402            let mut indexes = std::collections::BTreeMap::new();
403            let mut repos = Vec::new();
404            for (id, repo, state, definitions) in runs {
405                let index = match state {
406                    None => DefinitionIndex::none(),
407                    Some(state) => {
408                        match super::run_record::whole(&id, &state, definitions.as_deref()) {
409                            Ok(run) => DefinitionIndex::apart(&run),
410                            Err(unreadable) => {
411                                super::unreadable::skipped(&unreadable);
412                                continue;
413                            }
414                        }
415                    }
416                };
417                let repo = std::path::PathBuf::from(repo);
418                repos.push(repo.clone());
419                indexes.insert(id, (index, repo));
420            }
421            crate::workspace::outside_workspaces(repos.iter().map(|repo| repo.as_path()), &state)
422                .map_err(|error| Error::Invalid(error.to_string()))?;
423            let views = rows
424                .into_iter()
425                .filter_map(|row| {
426                    indexes
427                        .get_mut(&row.run)
428                        .and_then(|(index, repo)| row.view(index).map(|view| (view, repo.clone())))
429                })
430                .collect();
431            drop(statement);
432            transaction.commit()?;
433            Ok(views)
434        })
435        .await
436        .map_err(|e| e.to_string());
437    super::connection::finish(connection, result).await
438}
439
440/// Only present predicates are emitted, so Run and request lookups use their existing indexes.
441#[derive(Default)]
442pub(super) struct Query {
443    pub run: Option<crate::types::RunId>,
444    pub id: Option<crate::types::RequestId>,
445    pub repo: Option<String>,
446    pub waiting: bool,
447    pub session: Option<crate::types::SessionId>,
448    pub statuses: Vec<crate::types::RequestStatus>,
449    pub slice: Option<(u64, u32)>,
450    /// Only these requests.
451    pub ids: Option<Vec<crate::types::RequestId>>,
452}
453
454#[derive(Debug, Clone)]
455pub(super) struct Row {
456    pub id: crate::types::RequestId,
457    pub run: crate::types::RunId,
458    pub eval: crate::types::EvalId,
459    pub data: String,
460    pub execution: Option<String>,
461    pub execution_id: Option<crate::types::ExecutionId>,
462    pub claim: Option<(
463        crate::types::RequestId,
464        crate::types::ReviewerId,
465        crate::types::Timestamp,
466    )>,
467}
468
469pub(super) fn rows(db: &rusqlite::Connection, filter: &Query) -> Result<Vec<Row>, Error> {
470    let mut predicates = Vec::new();
471    let mut values: Vec<rusqlite::types::Value> = Vec::new();
472    for (column, value) in [
473        ("q.run_id", filter.run.as_ref().map(ToString::to_string)),
474        ("q.id", filter.id.as_ref().map(ToString::to_string)),
475        ("r.repo", filter.repo.clone()),
476        (
477            "json_extract(q.data,'$.sessionId')",
478            filter.session.as_ref().map(ToString::to_string),
479        ),
480    ] {
481        if let Some(value) = value {
482            predicates.push(format!("{column}=?"));
483            values.push(value.into());
484        }
485    }
486    if filter.waiting {
487        predicates.push("q.status IN ('WAITING_HUMAN','DEFERRED')".into());
488    }
489    if let Some(ids) = &filter.ids {
490        predicates.push("q.id IN (SELECT value FROM json_each(?))".into());
491        values.push(serde_json::to_string(ids)?.into());
492    }
493    if !filter.statuses.is_empty() {
494        let marks = vec!["?"; filter.statuses.len()].join(",");
495        predicates.push(format!("q.status IN ({marks})"));
496        values.extend(
497            filter
498                .statuses
499                .iter()
500                .map(|status| rusqlite::types::Value::from(status.as_str().to_owned())),
501        );
502    }
503    let predicate = if predicates.is_empty() {
504        String::new()
505    } else {
506        format!("WHERE {}", predicates.join(" AND "))
507    };
508    let limit = match filter.slice {
509        Some((offset, count)) => {
510            values.push(i64::from(count).into());
511            values.push(i64::try_from(offset).unwrap_or(i64::MAX).into());
512            " LIMIT ? OFFSET ?"
513        }
514        None => "",
515    };
516    let order = if filter.run.is_some() {
517        "q.ordinal"
518    } else {
519        "r.rowid DESC,q.ordinal"
520    };
521    let mut statement = db.prepare_cached(&format!(
522        "SELECT q.id,q.run_id,q.data,e.data,h.id,h.claimed_by,h.claimed_at,q.eval_id,e.id
523        FROM requests q JOIN runs r ON r.id=q.run_id
524        LEFT JOIN executions e ON e.id=q.execution_id
525        LEFT JOIN requests h ON h.id=json_extract(e.data,'$.provenance.requestId') AND h.claimed_by IS NOT NULL
526        {predicate} ORDER BY {order}{limit}"
527    ))?;
528    Ok(statement
529        .query_map(rusqlite::params_from_iter(values), |row| {
530            let id = row.get::<_, Option<crate::types::RequestId>>(4)?;
531            let reviewer = row.get::<_, Option<crate::types::ReviewerId>>(5)?;
532            let at = row.get::<_, Option<crate::types::Timestamp>>(6)?;
533            Ok(Row {
534                id: row.get(0)?,
535                run: row.get(1)?,
536                eval: row.get(7)?,
537                data: row.get(2)?,
538                execution: row.get(3)?,
539                execution_id: row.get(8)?,
540                claim: id
541                    .zip(reviewer)
542                    .zip(at)
543                    .map(|((id, reviewer), at)| (id, reviewer, at)),
544            })
545        })?
546        .collect::<Result<_, _>>()?)
547}
548
549/// Saved definitions are decoded once per Run and indexed by position, never copied whole.
550/// First duplicate Eval wins, matching SQLite's former json_each lookup; missing/null
551/// definitions remain absent.
552pub(super) struct DefinitionIndex {
553    graph: Option<Arc<Graph>>,
554    evals: HashMap<crate::types::EvalId, usize>,
555    /// The copies handed out so far, when each view keeps its own ([`Place::Own`]).
556    copies: Option<Copies>,
557}
558#[derive(Default)]
559struct Copies {
560    evals: HashMap<usize, Arc<super::definitions::Eval>>,
561    artifacts: HashMap<crate::types::ArtifactName, Arc<super::definitions::Artifact>>,
562}
563impl DefinitionIndex {
564    /// The views share the Run's graph: an opened Run's, read as one.
565    pub fn new(run: &super::Run) -> Self {
566        let graph = run.definitions.share();
567        let mut evals = HashMap::new();
568        for (position, eval) in graph.iter().flat_map(|graph| graph.evals()).enumerate() {
569            evals.entry(eval.id.clone()).or_insert(position);
570        }
571        Self {
572            graph,
573            evals,
574            copies: None,
575        }
576    }
577    /// Each view keeps copies of its own eval and Artifact, shared only by the views of this
578    /// read: requests read apart from their Runs.
579    fn apart(run: &super::Run) -> Self {
580        Self {
581            copies: Some(Copies::default()),
582            ..Self::new(run)
583        }
584    }
585    /// No definitions: the views show their requests alone.
586    fn none() -> Self {
587        Self {
588            graph: None,
589            evals: HashMap::new(),
590            copies: None,
591        }
592    }
593    fn definition(
594        &mut self,
595        eval: &crate::types::EvalId,
596        artifact: &crate::types::ArtifactName,
597    ) -> Option<RequestDefinition> {
598        let graph = self.graph.as_ref()?;
599        let saved = graph.artifact(artifact)?;
600        let position = *self.evals.get(eval)?;
601        Some(RequestDefinition(match &mut self.copies {
602            None => Place::Graph {
603                graph: graph.clone(),
604                eval: position,
605                artifact: artifact.clone(),
606            },
607            Some(copies) => Place::Own {
608                eval: copies
609                    .evals
610                    .entry(position)
611                    .or_insert_with(|| Arc::new(graph.evals()[position].clone()))
612                    .clone(),
613                artifact: copies
614                    .artifacts
615                    .entry(artifact.clone())
616                    .or_insert_with(|| Arc::new(saved.clone()))
617                    .clone(),
618            },
619        }))
620    }
621}
622impl Row {
623    pub fn view(&self, index: &mut DefinitionIndex) -> Option<RequestView> {
624        let request = super::unreadable::evidence::<Request>("request", &self.data)?;
625        let execution = self
626            .execution
627            .as_deref()
628            .and_then(|data| super::unreadable::evidence("execution", data));
629        Some(self.with_evidence(index, request, execution))
630    }
631    /// The request's view and why any of its records do not read. A request that does not read
632    /// has no view; an execution that does not read leaves the view without it.
633    pub fn read(
634        &self,
635        index: &mut DefinitionIndex,
636    ) -> (Option<RequestView>, Vec<super::Unreadable>) {
637        let mut unreadable = Vec::new();
638        let execution = self
639            .execution
640            .as_deref()
641            .zip(self.execution_id.as_ref())
642            .and_then(|(data, id)| {
643                super::unreadable::decode("execution", id, data)
644                    .map_err(|record| unreadable.push(record))
645                    .ok()
646            });
647        let view = super::unreadable::decode::<Request>("request", &self.id, &self.data)
648            .map_err(|record| unreadable.push(record))
649            .ok()
650            .map(|request| self.with_evidence(index, request, execution));
651        (view, unreadable)
652    }
653    fn with_evidence(
654        &self,
655        index: &mut DefinitionIndex,
656        request: Request,
657        execution: Option<Execution>,
658    ) -> RequestView {
659        let definition = index.definition(&self.eval, &request.target);
660        RequestView {
661            request: Arc::new(request),
662            definition,
663            execution: execution.map(Arc::new),
664            claim: self.claim.as_ref().map(|(id, reviewer, at)| HumanClaim {
665                request_id: id.clone(),
666                reviewer: reviewer.clone(),
667                claimed_at: *at,
668            }),
669        }
670    }
671}
672
673/// Both selected-Run view shapes from the same short read transaction and decoded rows.
674pub async fn read_run_requests(
675    state: &Path,
676    id: &str,
677) -> Result<(super::RunView, Vec<RequestView>), String> {
678    let state = state.to_path_buf();
679    let id: crate::types::RunId = id.parse()?;
680    crate::task::joined(
681        tokio::task::spawn_blocking(move || {
682            let mut reader = super::reader::Reader::default();
683            reader
684                .snapshot(&state, |db, cache, state| {
685                    cache.selected(db, state, &id, true)
686                })?
687                .ok_or_else(|| "Run not found.".into())
688        })
689        .await,
690    )
691    .map_err(|error| error.to_string())?
692}