Skip to main content

artifactize_core/store/
executions.rs

1mod mutex;
2
3use crate::types::{ExecutionStatus, Fingerprint, RequestId, ReuseKey, RunId};
4use std::{collections::BTreeMap, path::PathBuf};
5
6use rusqlite::{OptionalExtension, params};
7use serde::{Deserialize, Serialize};
8
9use super::{
10    Receipts, Request, history,
11    receipts::{Error, schema_initialized, update_request},
12};
13use crate::{config::Profile, process, runtime::Verdict};
14
15pub enum Claim {
16    Owned,
17    BudgetExhausted,
18    /// Every machine-wide slot of the execution's backend is held.
19    Full,
20    /// A live execution holds an equal, ancestor or descendant mutex tag.
21    Mutex {
22        eval_id: crate::types::EvalId,
23        run_id: crate::types::RunId,
24    },
25    Reuse(Box<Execution>),
26    Wait(crate::types::ExecutionId),
27    WaitHuman(crate::types::ExecutionId),
28}
29
30/// The machine-wide limit (`limits.json`) of the backend an execution would start on.
31pub struct Capacity {
32    pub limit: u32,
33    /// Whether the Run stopped admitting the backend. It is asked while the claim holds the
34    /// write lock, so the stop of a review that records it before it completes is always seen
35    /// once that review's slot is free.
36    pub stopped: Box<dyn FnOnce() -> bool + Send>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct Provenance {
42    #[serde(with = "crate::platform::path_serde")]
43    pub repo_path: PathBuf,
44    pub run_id: RunId,
45    pub request_id: RequestId,
46    pub eval_id: crate::types::EvalId,
47    pub eval_def_hash: crate::types::DefinitionHash,
48    pub completed_at: Option<crate::types::Timestamp>,
49    /// For Agent results: the SHA-256 of each tool's `executionPaths` when the review started,
50    /// by tool name and declared path.
51    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
52    pub execution_paths: crate::tools::pins::Pins,
53}
54
55/// One execution and, once GREEN or RED with a key, one record of its key's history. A local
56/// Agent execution holds one of its backend's machine-wide slots while RUNNING.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(try_from = "ExecutionWire", into = "ExecutionWire")]
59pub struct Execution {
60    pub state: super::ExecutionState,
61    pub id: crate::types::ExecutionId,
62    /// The reuse key; absent without a fingerprint on every Artifact the eval depends on.
63    pub key: Option<ReuseKey>,
64    /// The target Artifact's fingerprint.
65    pub fingerprint: Option<Fingerprint>,
66    /// Each Artifact the key covers, the target included, with its fingerprint.
67    pub fingerprints: BTreeMap<crate::types::ArtifactName, Fingerprint>,
68    /// Kinds of exactly the Artifacts covered by a new-format reuse key.
69    pub artifact_kinds: BTreeMap<crate::types::ArtifactName, crate::config::ArtifactKind>,
70    pub eval_def_hash: crate::types::DefinitionHash,
71    /// The local process running this execution; absent for one mirrored from a remote
72    /// review store, which no local process owns.
73    pub owner: Option<process::ChildIdentity>,
74    pub profile: crate::config::StoredProfile,
75    /// How this result was produced; never part of the key.
76    pub options: ExecutionOptions,
77    pub usage: Option<Vec<crate::llm::Attempt>>,
78    pub provenance: Provenance,
79    pub started_at: crate::types::Timestamp,
80    pub producer: Option<Producer>,
81    /// The Human claimant who submitted this result.
82    pub reviewer: Option<crate::types::ReviewerId>,
83    /// Set only on executions mirrored from a remote review store.
84    pub origin: Option<Origin>,
85    pub manifest: Option<crate::cache::Manifest>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub(crate) struct ExecutionWire {
91    pub id: crate::types::ExecutionId,
92    /// The reuse key; absent without a fingerprint on every Artifact the eval depends on.
93    #[serde(default)]
94    pub key: Option<ReuseKey>,
95    /// The target Artifact's fingerprint.
96    pub fingerprint: Option<Fingerprint>,
97    /// Each Artifact the key covers, the target included, with its fingerprint.
98    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
99    pub fingerprints: BTreeMap<crate::types::ArtifactName, Fingerprint>,
100    /// Kinds of exactly the Artifacts covered by a new-format reuse key.
101    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
102    pub artifact_kinds: BTreeMap<crate::types::ArtifactName, crate::config::ArtifactKind>,
103    pub eval_def_hash: crate::types::DefinitionHash,
104    pub owner_pid: u32,
105    pub owner_start_time: u64,
106    pub status: ExecutionStatus,
107    pub result: Option<super::ExecutionResult>,
108    pub error: Option<String>,
109    pub error_code: Option<crate::types::FailureCode>,
110    pub profile: crate::config::StoredProfile,
111    /// How this result was produced; never part of the key.
112    #[serde(default)]
113    pub options: ExecutionOptions,
114    pub usage: Option<Vec<crate::llm::Attempt>>,
115    pub provenance: Provenance,
116    pub started_at: crate::types::Timestamp,
117    pub completed_at: Option<crate::types::Timestamp>,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub producer: Option<Producer>,
120    /// The Human claimant who submitted this result.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub reviewer: Option<crate::types::ReviewerId>,
123    /// Set only on executions mirrored from a remote review store.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub origin: Option<Origin>,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub manifest: Option<crate::cache::Manifest>,
128}
129
130impl TryFrom<ExecutionWire> for Execution {
131    type Error = String;
132    fn try_from(wire: ExecutionWire) -> Result<Self, Self::Error> {
133        let state = super::ExecutionState::from_parts(
134            wire.status,
135            wire.result,
136            wire.error,
137            wire.error_code,
138            wire.completed_at,
139        )?;
140        Ok(Self {
141            state,
142            id: wire.id,
143            key: wire.key,
144            fingerprint: wire.fingerprint,
145            fingerprints: wire.fingerprints,
146            artifact_kinds: wire.artifact_kinds,
147            eval_def_hash: wire.eval_def_hash,
148            owner: std::num::NonZeroU32::new(wire.owner_pid).map(|pid| process::ChildIdentity {
149                pid: process::ProcessId::from(pid),
150                start_time: wire.owner_start_time,
151            }),
152            profile: wire.profile,
153            options: wire.options,
154            usage: wire.usage,
155            provenance: wire.provenance,
156            started_at: wire.started_at,
157            producer: wire.producer,
158            reviewer: wire.reviewer,
159            origin: wire.origin,
160            manifest: wire.manifest,
161        })
162    }
163}
164impl From<Execution> for ExecutionWire {
165    fn from(record: Execution) -> Self {
166        let owner_pid = record.owner_pid();
167        let owner_start_time = record.owner_start_time();
168        Self {
169            status: record.status(),
170            result: record.result().cloned(),
171            error: record.error().map(str::to_owned),
172            error_code: record.error_code(),
173            completed_at: record.completed_at(),
174            id: record.id,
175            key: record.key,
176            fingerprint: record.fingerprint,
177            fingerprints: record.fingerprints,
178            artifact_kinds: record.artifact_kinds,
179            eval_def_hash: record.eval_def_hash,
180            owner_pid,
181            owner_start_time,
182            profile: record.profile,
183            options: record.options,
184            usage: record.usage,
185            provenance: record.provenance,
186            started_at: record.started_at,
187            producer: record.producer,
188            reviewer: record.reviewer,
189            origin: record.origin,
190            manifest: record.manifest,
191        }
192    }
193}
194impl Execution {
195    pub fn status(&self) -> crate::types::ExecutionStatus {
196        self.state.status()
197    }
198    pub fn result(&self) -> Option<&super::ExecutionResult> {
199        self.state.result()
200    }
201    pub fn error(&self) -> Option<&str> {
202        self.state.error()
203    }
204    pub fn error_code(&self) -> Option<crate::types::FailureCode> {
205        self.state.error_code()
206    }
207    pub fn completed_at(&self) -> Option<crate::types::Timestamp> {
208        self.state.completed_at()
209    }
210    /// The owning process's pid, or 0 for a remote-mirrored execution with no local owner:
211    /// the `owner_pid` SQL column and wire field's sentinel for "no owner", confined to
212    /// this persistence boundary.
213    pub fn owner_pid(&self) -> u32 {
214        self.owner.map_or(0, |owner| owner.pid.get())
215    }
216    /// The owning process's start time, or 0 alongside [`Self::owner_pid`]'s sentinel.
217    pub fn owner_start_time(&self) -> u64 {
218        self.owner.map_or(0, |owner| owner.start_time)
219    }
220}
221
222/// The execution options of a result: stored next to it, never part of its key, so results
223/// from different profiles for the same key reuse each other.
224#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct ExecutionOptions {
227    #[serde(default, skip_serializing_if = "Vec::is_empty")]
228    pub mutex: Vec<crate::config::MutexTag>,
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub backend: Option<crate::config::Backend>,
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub model: Option<crate::config::ModelId>,
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub reasoning: Option<crate::config::Reasoning>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    #[serde(with = "crate::config::validation::milliseconds")]
237    pub timeout_ms: Option<std::time::Duration>,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub max_tool_calls: Option<u64>,
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub max_tokens: Option<u64>,
242    /// The selected `profileVariants` entry; absent for the declared profile.
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub variant: Option<crate::config::ProfileVariantName>,
245}
246
247impl ExecutionOptions {
248    /// The declared options of an effective profile, as declared (absent means the default).
249    pub fn new(profile: &Profile, variant: Option<&crate::config::ProfileVariantName>) -> Self {
250        let mut options = Self {
251            variant: variant.cloned(),
252            ..Self::default()
253        };
254        match profile {
255            Profile::Agent {
256                backend,
257                model,
258                reasoning,
259                timeout_ms,
260                max_tool_calls,
261                max_tokens,
262            } => {
263                options.backend = Some(backend.clone());
264                options.model = Some(model.clone());
265                options.reasoning = *reasoning;
266                options.timeout_ms = *timeout_ms;
267                options.max_tool_calls = *max_tool_calls;
268                options.max_tokens = *max_tokens;
269            }
270            Profile::Runtime { timeout_ms, .. } => options.timeout_ms = *timeout_ms,
271            Profile::Human {} => {}
272        }
273        options
274    }
275}
276
277/// Who produced an execution: display metadata, never authentication.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct Producer {
280    /// `user@host`.
281    pub name: crate::types::ProducerId,
282    /// The producing artifactize version.
283    pub version: String,
284    /// Where an Agent review's saved conversation lives. It travels with remote records,
285    /// which older clients read without it; the conversation itself never leaves the machine.
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub session: Option<crate::agent::session::SessionRef>,
288}
289
290/// The name every producer without a user or host gets.
291const UNKNOWN_PRODUCER: &str = "unknown@unknown";
292
293impl Producer {
294    pub fn current() -> Self {
295        let user = crate::platform::user_name();
296        let host = crate::platform::host_name();
297        let name = format!(
298            "{}@{}",
299            user.as_deref().unwrap_or("unknown"),
300            host.as_deref()
301                .map(str::trim)
302                .filter(|host| !host.is_empty())
303                .unwrap_or("unknown")
304        );
305        // A name is printable and bounded; whatever the system reports is cut to fit.
306        let mut printable: String = name.chars().filter(|c| !c.is_control()).collect();
307        while printable.len() > crate::types::MAX_ID_BYTES {
308            printable.pop();
309        }
310        Self {
311            name: printable
312                .parse()
313                .unwrap_or_else(|_| UNKNOWN_PRODUCER.parse().expect("a valid producer name")),
314            version: env!("CARGO_PKG_VERSION").into(),
315            session: None,
316        }
317    }
318}
319
320/// The remote store, authenticated publisher and server clock of a mirrored execution.
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct Origin {
324    pub store: crate::types::StoreUrl,
325    pub publisher: crate::types::TokenName,
326    pub published_at: crate::types::Timestamp,
327}
328
329impl Execution {
330    /// The backend whose machine-wide slot this execution holds while RUNNING: a local Agent
331    /// review's. A mirrored record ran elsewhere.
332    fn backend(&self) -> Option<&str> {
333        self.options
334            .backend
335            .as_ref()
336            .map(|backend| backend.as_str())
337            .filter(|_| self.origin.is_none())
338    }
339
340    /// Whether this record completed after `other`, comparing `completedAt` as instants.
341    pub fn completed_after(&self, other: &Execution) -> bool {
342        self.completed_at() > other.completed_at()
343    }
344
345    pub fn verdict(&self) -> Option<Verdict> {
346        match self.status() {
347            ExecutionStatus::Green => Some(Verdict::Green),
348            ExecutionStatus::Red => Some(Verdict::Red),
349            _ => None,
350        }
351    }
352}
353
354/// The latest completed record of a key.
355pub(super) fn lookup(
356    db: &rusqlite::Connection,
357    key: &ReuseKey,
358) -> Result<Option<Execution>, Error> {
359    let data: Option<String> = db
360        .query_row(
361            &format!(
362                "SELECT e.data FROM executions e WHERE e.key=? AND {} ORDER BY {} LIMIT 1",
363                history::RECORD,
364                history::LATEST
365            ),
366            [key],
367            |row| row.get(0),
368        )
369        .optional()?;
370    Ok(data.and_then(|data| super::unreadable::evidence("execution", &data)))
371}
372
373/// End a RUNNING execution whose owner process is gone, which frees its key and its slot.
374fn owner_died(
375    db: &rusqlite::Connection,
376    id: &crate::types::ExecutionId,
377    at: crate::types::Timestamp,
378) -> Result<(), Error> {
379    db.execute(
380        "UPDATE executions SET status='ERROR',data=json_set(data,'$.status','ERROR','$.error','Execution owner died.','$.errorCode','OWNER_DIED','$.result',NULL,'$.completedAt',?1,'$.provenance.completedAt',?1) WHERE id=?2 AND status='RUNNING'",
381        params![at, id],
382    )?;
383    Ok(())
384}
385
386/// How many slots of `backend` the RUNNING executions of live owners hold; the executions of
387/// dead owners end, freeing theirs.
388fn held_slots(
389    db: &rusqlite::Connection,
390    backend: &str,
391    at: crate::types::Timestamp,
392) -> Result<u32, Error> {
393    let owners = {
394        let mut statement = db.prepare(
395            "SELECT id,owner_pid,owner_start_time FROM executions WHERE backend=? AND status='RUNNING'",
396        )?;
397        statement
398            .query_map([backend], |row| {
399                Ok((
400                    row.get::<_, crate::types::ExecutionId>(0)?,
401                    process::ChildIdentity {
402                        pid: row.get(1)?,
403                        start_time: row.get::<_, i64>(2)? as u64,
404                    },
405                ))
406            })?
407            .collect::<Result<Vec<_>, _>>()?
408    };
409    let mut held = 0;
410    for (id, owner) in owners {
411        if process::is_alive(Some(owner)).map_err(|e| Error::Invalid(e.to_string()))? {
412            held += 1;
413        } else {
414            owner_died(db, &id, at)?;
415        }
416    }
417    Ok(held)
418}
419
420/// Corruption must not reserve a reuse key or machine-wide slot forever. Preserve its
421/// original JSON for the unreadable audit, but retire the invalid active index entry.
422fn retire_unreadable(
423    db: &rusqlite::Connection,
424    key: Option<&ReuseKey>,
425    backend: Option<&str>,
426) -> Result<(), Error> {
427    let records = {
428        let mut statement = db.prepare("SELECT id,data FROM executions WHERE status IN ('RUNNING','WAITING_HUMAN') AND ((?1 IS NOT NULL AND key=?1) OR (?2 IS NOT NULL AND backend=?2))")?;
429        statement
430            .query_map(params![key, backend], |row| {
431                Ok((
432                    row.get::<_, crate::types::ExecutionId>(0)?,
433                    row.get::<_, String>(1)?,
434                ))
435            })?
436            .collect::<Result<Vec<_>, _>>()?
437    };
438    for (id, data) in records {
439        if let Err(error) = super::unreadable::decode::<Execution>("execution", id.as_str(), &data)
440        {
441            use std::io::Write;
442            let _ = writeln!(std::io::stderr().lock(), "{error}");
443            db.execute(
444                "UPDATE executions SET status='ERROR',backend=NULL WHERE id=?",
445                [id],
446            )?;
447        }
448    }
449    Ok(())
450}
451
452/// The complete declared effective profile an in-flight execution is joined on, as its
453/// stored JSON. Only [`ProfileIdentity::of`] makes one, so every join compares one spelling.
454#[derive(Clone, Debug, PartialEq, Eq)]
455struct ProfileIdentity(String);
456
457impl ProfileIdentity {
458    fn of(profile: &crate::config::StoredProfile) -> Result<Self, Error> {
459        Ok(Self(serde_json::to_string(profile)?))
460    }
461}
462
463impl rusqlite::ToSql for ProfileIdentity {
464    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
465        self.0.to_sql()
466    }
467}
468
469/// The one definition of in-flight identity. Completed evidence still uses only its
470/// reuse key; joining additionally requires the complete declared effective profile.
471fn sharing_identity(
472    key: &ReuseKey,
473    profile: &crate::config::StoredProfile,
474) -> Result<(ReuseKey, ProfileIdentity), Error> {
475    Ok((key.clone(), ProfileIdentity::of(profile)?))
476}
477
478fn active_owner(
479    db: &rusqlite::Connection,
480    key: &ReuseKey,
481    profile: Option<&ProfileIdentity>,
482) -> Result<
483    Option<(
484        crate::types::ExecutionId,
485        process::ChildIdentity,
486        ExecutionStatus,
487    )>,
488    Error,
489> {
490    let row = db
491        .query_row(
492            "SELECT id,owner_pid,owner_start_time,status,data FROM executions WHERE key=?1 AND (?2 IS NULL OR profile_identity=?2) AND status IN ('RUNNING','WAITING_HUMAN')",
493            params![key, profile],
494            |row| {
495                Ok((
496                    row.get::<_, crate::types::ExecutionId>(0)?,
497                    process::ChildIdentity {
498                        pid: row.get(1)?,
499                        start_time: row.get::<_, i64>(2)? as u64,
500                    },
501                    row.get::<_, ExecutionStatus>(3)?,
502                    row.get::<_, String>(4)?,
503                ))
504            },
505        )
506        .optional()?;
507    Ok(row.and_then(|(id, owner, status, data)| {
508        super::unreadable::evidence::<Execution>("execution", &data).map(|_| (id, owner, status))
509    }))
510}
511
512fn available_to_waiter(
513    db: &rusqlite::Connection,
514    key: &ReuseKey,
515    waiting_for: Option<&crate::types::ExecutionId>,
516    profile: &ProfileIdentity,
517) -> Result<Option<Claim>, Error> {
518    if let Some(id) = waiting_for {
519        let data: Option<String> = db
520            .query_row(
521                "SELECT data FROM executions WHERE id=? AND key=? AND profile_identity=?",
522                params![id, key, profile],
523                |row| row.get(0),
524            )
525            .optional()?;
526        if let Some(data) = data
527            && let Some(execution) = super::unreadable::evidence::<Execution>("execution", &data)
528        {
529            match execution.status() {
530                ExecutionStatus::Green | ExecutionStatus::Red => {
531                    return Ok(Some(Claim::Reuse(Box::new(execution))));
532                }
533                ExecutionStatus::Error
534                    if !matches!(
535                        execution.error_code(),
536                        Some(
537                            crate::types::FailureCode::Cancelled
538                                | crate::types::FailureCode::OwnerDied
539                        )
540                    ) =>
541                {
542                    return Ok(Some(Claim::Reuse(Box::new(execution))));
543                }
544                // A joined request stays with its execution, even if an independent
545                // forced or different-profile review publishes another result meanwhile.
546                ExecutionStatus::Running
547                    if process::is_alive(execution.owner)
548                        .map_err(|error| Error::Invalid(error.to_string()))? =>
549                {
550                    return Ok(Some(Claim::Wait(execution.id)));
551                }
552                ExecutionStatus::WaitingHuman => return Ok(Some(Claim::WaitHuman(execution.id))),
553                _ => {}
554            }
555        }
556    }
557    available(db, key, Some(profile))
558}
559
560fn available(
561    db: &rusqlite::Connection,
562    key: &ReuseKey,
563    profile: Option<&ProfileIdentity>,
564) -> Result<Option<Claim>, Error> {
565    if let Some(execution) = lookup(db, key)? {
566        return Ok(Some(Claim::Reuse(Box::new(execution))));
567    }
568    if let Some((id, owner, status)) = active_owner(db, key, profile)? {
569        if status == ExecutionStatus::WaitingHuman {
570            return Ok(Some(Claim::WaitHuman(id)));
571        }
572        if process::is_alive(Some(owner)).map_err(|e| Error::Invalid(e.to_string()))? {
573            return Ok(Some(Claim::Wait(id)));
574        }
575    }
576    Ok(None)
577}
578
579async fn open_read_only(
580    state: &std::path::Path,
581) -> Result<Option<tokio_rusqlite::Connection>, String> {
582    let state = crate::workspace::canonical_target(state).map_err(|e| e.to_string())?;
583    super::receipts::check_files(&state)?;
584    if !state
585        .join(super::DATABASE)
586        .try_exists()
587        .map_err(|e| e.to_string())?
588    {
589        return Ok(None);
590    }
591    tokio_rusqlite::Connection::open_with_flags(
592        state.join(super::DATABASE),
593        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
594    )
595    .await
596    .map(Some)
597    .map_err(|e| e.to_string())
598}
599
600/// Read the latest completed record or the live owner of each key without creating or
601/// changing the database.
602pub async fn read_keyed_executions(
603    state: &std::path::Path,
604    keys: &[ReuseKey],
605) -> Result<std::collections::BTreeMap<ReuseKey, Claim>, String> {
606    read_claims(
607        state,
608        keys.iter()
609            .map(|key| (key.clone(), key.clone(), None))
610            .collect(),
611    )
612    .await
613}
614
615pub(crate) async fn read_profiled_executions(
616    state: &std::path::Path,
617    keys: Vec<(crate::types::EvalId, ReuseKey, crate::config::StoredProfile)>,
618) -> Result<BTreeMap<crate::types::EvalId, Claim>, String> {
619    let keys = keys
620        .into_iter()
621        .map(|(id, key, profile)| {
622            sharing_identity(&key, &profile)
623                .map(|(key, profile)| (id, key, Some(profile)))
624                .map_err(|error| error.to_string())
625        })
626        .collect::<Result<Vec<_>, _>>()?;
627    read_claims(state, keys).await
628}
629
630async fn read_claims<K: Ord + Send + 'static>(
631    state: &std::path::Path,
632    keys: Vec<(K, ReuseKey, Option<ProfileIdentity>)>,
633) -> Result<BTreeMap<K, Claim>, String> {
634    if keys.is_empty() {
635        return Ok(Default::default());
636    }
637    let Some(connection) = open_read_only(state).await? else {
638        return Ok(Default::default());
639    };
640    let result = connection
641        .call(move |db| -> Result<_, Error> {
642            super::wait_when_busy(db)?;
643            let transaction = db.transaction()?;
644            if !schema_initialized(&transaction)? {
645                return Ok(Default::default());
646            }
647            let mut entries = std::collections::BTreeMap::new();
648            for (id, key, profile) in keys {
649                if let Some(claim) = available(&transaction, &key, profile.as_ref())? {
650                    entries.insert(id, claim);
651                }
652            }
653            transaction.commit()?;
654            Ok(entries)
655        })
656        .await
657        .map_err(|e| e.to_string());
658    super::connection::finish(connection, result).await
659}
660
661/// The latest record per (Eval id, Eval definition hash) under any key, read-only: what a
662/// changed key is explained against.
663pub async fn read_latest_cached(
664    state: &std::path::Path,
665    keys: &[(crate::types::EvalId, crate::types::DefinitionHash)],
666) -> Result<
667    std::collections::BTreeMap<(crate::types::EvalId, crate::types::DefinitionHash), Execution>,
668    String,
669> {
670    if keys.is_empty() {
671        return Ok(Default::default());
672    }
673    let Some(connection) = open_read_only(state).await? else {
674        return Ok(Default::default());
675    };
676    let keys = keys.to_vec();
677    let result = connection
678        .call(move |db| -> Result<_, Error> {
679            super::wait_when_busy(db)?;
680            let transaction = db.transaction()?;
681            if !schema_initialized(&transaction)? {
682                return Ok(Default::default());
683            }
684            let mut latest = std::collections::BTreeMap::new();
685            {
686                let mut statement = transaction.prepare(&format!(
687                    "SELECT e.data FROM executions e WHERE e.eval_def_hash=? AND json_extract(e.data,'$.provenance.evalId')=? AND {} ORDER BY {} LIMIT 1",
688                    history::RECORD,
689                    history::LATEST
690                ))?;
691                for (eval_id, eval_def_hash) in keys {
692                    let data: Option<String> = statement
693                        .query_row(params![eval_def_hash, eval_id], |row| row.get(0))
694                        .optional()?;
695                    if let Some(data) = data
696                        && let Some(execution) = super::unreadable::evidence("execution", &data)
697                    {
698                        latest.insert((eval_id, eval_def_hash), execution);
699                    }
700                }
701            }
702            transaction.commit()?;
703            Ok(latest)
704        })
705        .await
706        .map_err(|e| e.to_string());
707    super::connection::finish(connection, result).await
708}
709
710impl Receipts {
711    /// The key's latest record; a hit updates its last use.
712    pub async fn cached_execution(&self, key: &str) -> Result<Option<Execution>, String> {
713        let key: ReuseKey = key.parse()?;
714        self.connection
715            .call(move |db| -> Result<_, Error> {
716                let execution = lookup(db, &key)?;
717                if let Some(execution) = &execution {
718                    history::touch(db, &execution.id, crate::broker::now())?;
719                }
720                Ok(execution)
721            })
722            .await
723            .map_err(|e| e.to_string())
724    }
725
726    /// The saved execution eligible for publication; speculative Runs hold publication
727    /// until blocked work has been withdrawn. Mirrors never publish again.
728    pub async fn published_execution(
729        &self,
730        execution_id: &str,
731    ) -> Result<Option<Execution>, String> {
732        let execution_id: crate::types::ExecutionId = execution_id.parse()?;
733        self.connection
734            .call(move |db| -> Result<_, Error> {
735                let data: Option<String> = db
736                    .query_row(
737                        &format!(
738                            "SELECT e.data FROM executions e LEFT JOIN runs r ON r.id=json_extract(e.data,'$.provenance.runId') WHERE e.id=? AND {} AND (r.id IS NULL OR r.status NOT IN ('RUNNING','WAITING_HUMAN') OR coalesce(json_extract(r.data,'$.modes.speculate'),0)=0 OR json_extract(r.data,'$.control.activity')='publishing')",
739                            history::RECORD
740                        ),
741                        [execution_id],
742                        |row| row.get(0),
743                    )
744                    .optional()?;
745                Ok(data
746                    .and_then(|data| super::unreadable::evidence::<Execution>("execution", &data))
747                    .filter(|execution| execution.origin.is_none()))
748            })
749            .await
750            .map_err(|e| e.to_string())
751    }
752
753    /// Claim an execution for its request. A `keyed` claim (a key, not forced) first reuses
754    /// the key's latest record or waits for its live execution with the same profile. Starting needs
755    /// `allow_start` and, on a backend with a machine-wide limit, a free slot: the slots the
756    /// backend's RUNNING executions hold are counted and this execution inserted RUNNING in
757    /// one write transaction. Mutex tags are also checked and held in that transaction,
758    /// including for unkeyed and forced executions. An unkeyed or forced execution without
759    /// a limit or mutex tags is saved when it completes.
760    pub async fn claim_execution(
761        &self,
762        execution: &Execution,
763        keyed: bool,
764        waiting_for: Option<&crate::types::ExecutionId>,
765        allow_start: bool,
766        capacity: Option<Capacity>,
767    ) -> Result<Claim, String> {
768        let execution = execution.clone();
769        let waiting_for = waiting_for.cloned();
770        let identity = execution
771            .key
772            .as_ref()
773            .filter(|_| keyed)
774            .map(|key| sharing_identity(key, &execution.profile))
775            .transpose()
776            .map_err(|error| error.to_string())?;
777        self.connection
778            .call(move |db| -> Result<Claim, Error> {
779                let key = identity.as_ref().map(|(key, _)| key);
780                let profile = identity.as_ref().map(|(_, profile)| profile);
781                if let Some(key) = key {
782                    let transaction = db.transaction()?;
783                    if let Some(claim) =
784                        available_to_waiter(&transaction, key, waiting_for.as_ref(), profile.expect("keyed identity"))?
785                    {
786                        return Ok(claim);
787                    }
788                }
789                if !allow_start && key.is_none() {
790                    return Ok(Claim::BudgetExhausted);
791                }
792                let capacity = capacity.zip(execution.backend());
793                if key.is_none() && capacity.is_none() && execution.options.mutex.is_empty() {
794                    return Ok(Claim::Owned);
795                }
796                let transaction =
797                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
798                if let Some(key) = key
799                    && let Some(claim) =
800                        available_to_waiter(&transaction, key, waiting_for.as_ref(), profile.expect("keyed identity"))?
801                {
802                    return Ok(claim);
803                }
804                // The read fast path may have observed RUNNING before publication and
805                // then a dead publisher. Confirm availability in a fresh write snapshot
806                // even when this waiter has no budget to start another execution.
807                if !allow_start {
808                    return Ok(Claim::BudgetExhausted);
809                }
810                retire_unreadable(&transaction, key, capacity.as_ref().map(|(_, backend)| *backend))?;
811                if !execution.options.mutex.is_empty()
812                    && let Some(conflict) = mutex::conflict(&transaction, &execution)?
813                {
814                    transaction.commit()?;
815                    return Ok(conflict);
816                }
817                if let Some((capacity, backend)) = capacity {
818                    if (capacity.stopped)() {
819                        return Ok(Claim::BudgetExhausted);
820                    }
821                    if held_slots(&transaction, backend, execution.started_at)? >= capacity.limit {
822                        transaction.commit()?;
823                        return Ok(Claim::Full);
824                    }
825                }
826                if let Some(key) = key
827                    && let Some((id, _, _)) = active_owner(&transaction, key, profile)?
828                {
829                    owner_died(&transaction, &id, execution.started_at)?;
830                }
831                let inserted = transaction.execute(
832                    "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,backend,profile_identity,data) VALUES (?,?,?,?,?,?,?,?,?) ON CONFLICT(key,profile_identity) WHERE key IS NOT NULL AND status IN ('RUNNING','WAITING_HUMAN') DO NOTHING",
833                    params![
834                        execution.id,
835                        key,
836                        execution.eval_def_hash,
837                        execution.status(),
838                        execution.owner_pid(),
839                        execution.owner_start_time() as i64,
840                        execution.backend(),
841                        profile,
842                        serde_json::to_string(&execution)?
843                    ],
844                )?;
845                let claim = match key {
846                    Some(key) if inserted == 0 => Claim::Wait(
847                        active_owner(&transaction, key, profile)?
848                            .expect("conflicting active key")
849                            .0,
850                    ),
851                    _ => Claim::Owned,
852                };
853                transaction.commit()?;
854                Ok(claim)
855            })
856            .await
857            .map_err(|e| e.to_string())
858    }
859
860    pub async fn reuse_execution(&self, request: &Request) -> Result<(), String> {
861        let request = request.clone();
862        self.connection
863            .call(move |db| -> Result<(), Error> {
864                let transaction = db.transaction()?;
865                update_request(&transaction, &request)?;
866                if let (Some(execution), Some(at)) = (&request.execution_id, request.completed_at())
867                {
868                    history::touch(&transaction, execution, at)?;
869                }
870                transaction.commit()?;
871                Ok(())
872            })
873            .await
874            .map_err(|e| e.to_string())
875    }
876
877    pub async fn complete_execution(
878        &self,
879        execution: &Execution,
880        request: &Request,
881    ) -> Result<(), String> {
882        let execution = execution.clone();
883        let request = request.clone();
884        self.connection
885            .call(move |db| -> Result<(), Error> {
886                let transaction =
887                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
888                settle(&transaction, &execution, &request)?;
889                transaction.commit()?;
890                Ok(())
891            })
892            .await
893            .map_err(|e| e.to_string())
894    }
895
896    /// Add a remote result to its key's history as a self-contained execution when it
897    /// completed after the key's latest local record, which it then replaces as the one reused;
898    /// `None` when the local record is as new. The mirrored record settles a local Human wait
899    /// for the key.
900    pub async fn mirror_execution(
901        &self,
902        execution: &Execution,
903    ) -> Result<Option<Execution>, String> {
904        let execution = execution.clone();
905        self.connection
906            .call(move |db| -> Result<Option<Execution>, Error> {
907                let (Some(key), Some(_), Some(_)) =
908                    (&execution.key, &execution.origin, execution.verdict())
909                else {
910                    return Err(Error::Invalid(
911                        "Only completed remote GREEN/RED results can be mirrored.".into(),
912                    ));
913                };
914                let data = serde_json::to_string(&execution)?;
915                if data.len() > history::MAX_ENTRY_BYTES {
916                    return Err(Error::Invalid(
917                        "Remote result exceeds the cache entry limit.".into(),
918                    ));
919                }
920                let transaction =
921                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
922                if lookup(&transaction, key)?
923                    .is_some_and(|local| !execution.completed_after(&local))
924                {
925                    transaction.commit()?;
926                    return Ok(None);
927                }
928                // A mirror kept after `cache rm` or GC is reused for the same remote execution.
929                transaction.execute(
930                    "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,data) VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING",
931                    params![
932                        execution.id,
933                        key,
934                        execution.eval_def_hash,
935                        execution.status(),
936                        execution.owner_pid(),
937                        execution.owner_start_time() as i64,
938                        data
939                    ],
940                )?;
941                let data: String = transaction
942                    .query_row(
943                        "SELECT data FROM executions WHERE id=? AND key=? AND status=?",
944                        params![execution.id, key, execution.status()],
945                        |row| row.get(0),
946                    )
947                    .optional()?
948                    .ok_or_else(|| {
949                        Error::Invalid(
950                            "Mirrored execution ID conflicts with another execution.".into(),
951                        )
952                    })?;
953                let Some(mirrored) = super::unreadable::evidence::<Execution>("execution", &data) else {
954                    transaction.commit()?;
955                    return Ok(None);
956                };
957                if let Some((completed_at, bytes)) = history::columns(&mirrored, data.len())? {
958                    transaction.execute(
959                        "UPDATE executions SET completed_at=?,bytes=?,last_used=? WHERE id=? AND completed_at IS NULL",
960                        params![completed_at, bytes, crate::broker::now(), mirrored.id],
961                    )?;
962                }
963                super::human::settle_waiting(&transaction, &mirrored)?;
964                transaction.commit()?;
965                Ok(Some(mirrored))
966            })
967            .await
968            .map_err(|e| e.to_string())
969    }
970}
971
972/// Save a finished execution and its request; a GREEN/RED with a key joins the key's history.
973/// A claimed or Human execution updates its active row; a forced or unkeyed one is inserted,
974/// or replaces the RUNNING row its limited start inserted.
975pub(super) fn settle(
976    db: &rusqlite::Connection,
977    execution: &Execution,
978    request: &Request,
979) -> Result<(), Error> {
980    if request.status() != execution.status().into()
981        || request.execution_id.as_ref() != Some(&execution.id)
982        || request.result() != execution.result()
983        || request.error() != execution.error()
984        || request.error_code() != execution.error_code()
985    {
986        return Err(Error::Invalid(
987            "Execution and request settlement disagree.".into(),
988        ));
989    }
990    let data = serde_json::to_string(execution)?;
991    let record = history::columns(execution, data.len())?;
992    let last_used = record.as_ref().and(execution.completed_at());
993    let (completed_at, bytes) = record.unzip();
994    let claimed = execution.key.is_some() && !request.force;
995    if claimed || request.human_definition.is_some() {
996        if db.execute(
997            "UPDATE executions SET key=?,status=?,completed_at=?,bytes=?,last_used=?,data=? WHERE id=? AND owner_pid=? AND owner_start_time=? AND status IN ('RUNNING','WAITING_HUMAN')",
998            params![
999                execution.key,
1000                execution.status(),
1001                completed_at,
1002                bytes,
1003                last_used,
1004                data,
1005                execution.id,
1006                execution.owner_pid(),
1007                execution.owner_start_time() as i64
1008            ],
1009        )? != 1
1010        {
1011            return Err(Error::Invalid("Active execution not found.".into()));
1012        }
1013    } else {
1014        db.execute(
1015            "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,backend,completed_at,bytes,last_used,data) VALUES (?,?,?,?,?,?,?,?,?,?,?)
1016            ON CONFLICT(id) DO UPDATE SET key=excluded.key,status=excluded.status,completed_at=excluded.completed_at,bytes=excluded.bytes,last_used=excluded.last_used,data=excluded.data",
1017            params![
1018                execution.id,
1019                execution.key,
1020                execution.eval_def_hash,
1021                execution.status(),
1022                execution.owner_pid(),
1023                execution.owner_start_time() as i64,
1024                execution.backend(),
1025                completed_at,
1026                bytes,
1027                last_used,
1028                data
1029            ],
1030        )?;
1031    }
1032    update_request(db, request)
1033}