Skip to main content

artifactize_core/store/
executions.rs

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