Skip to main content

artifactize_core/store/
requests.rs

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