Skip to main content

artifactize_core/store/
requests.rs

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