Skip to main content

recall_worker/
lib.rs

1//! `recall-worker`: the process that merges, so the one facing the internet
2//! does not have to.
3//!
4//! It is an enrolled device with the `worker` scope, running beside
5//! `recall-server` (in the same compose file, as a second service) with no
6//! inbound port. It long-polls the server's merge queue, reconciles each
7//! job's two versions with the local `claude` CLI, and posts the result
8//! back. The `claude` login lives on the worker's own volume, so a
9//! compromise of the API process no longer reaches it. See
10//! `docs/design/part5-plan.md`, "The worker", and the "Jobs" section of
11//! `docs/reference/api.md`.
12//!
13//! No Anthropic API key appears anywhere here, as anywhere in Recall: the
14//! merge rides whatever account the CLI on this machine is logged in to.
15//!
16//! The crate has two unconditional parts, so `recall-server` and (later)
17//! `recall`'s own CLI can use them without the job loop: [`merge`] is the
18//! merge itself, the prompt and the flags that keep it cheap, and
19//! [`redact`] finds and masks secrets in memory text
20//! (`docs/history/memory-truth.md` decision 2). Everything else is behind
21//! the `client` feature (on by default): enrolling, signing requests, and
22//! the job loop, with the HTTP client they need. The server turns it off.
23
24#![deny(missing_docs)]
25
26pub mod merge;
27pub mod redact;
28
29#[cfg(feature = "client")]
30pub mod api;
31#[cfg(feature = "client")]
32pub mod config;
33#[cfg(feature = "client")]
34pub mod evaluate;
35#[cfg(feature = "client")]
36pub mod identity;
37#[cfg(feature = "client")]
38pub mod worker;
39
40pub use merge::Merger;
41pub use redact::Redactor;
42
43use time::OffsetDateTime;
44
45/// A timestamp in the format every Recall API answer uses: JavaScript's
46/// `Date.toISOString()`, millisecond precision with a `Z`, such as
47/// `2026-09-03T21:49:55.191Z`. The same function as `recall_server::now`,
48/// kept here so the merge does not depend on the server.
49pub fn now() -> String {
50    let fmt = time::macros::format_description!(
51        "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
52    );
53    OffsetDateTime::now_utc()
54        .format(&fmt)
55        .expect("the timestamp format is a compile-time constant")
56}
57
58/// What `recall-worker` sends as its `User-Agent`, and so what `/health`
59/// shows as the worker's agent: `recall-worker/0.4.2 (linux-x86_64)`.
60pub fn user_agent() -> String {
61    format!(
62        "recall-worker/{} ({}-{})",
63        recall_wire::discovery::version(),
64        std::env::consts::OS,
65        std::env::consts::ARCH
66    )
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn timestamps_match_javascripts_toisostring() {
75        let t = now();
76        assert_eq!(t.len(), 24, "got {t}");
77        assert!(t.ends_with('Z') && &t[10..11] == "T" && &t[19..20] == ".");
78    }
79
80    #[test]
81    fn the_agent_names_the_worker() {
82        assert!(
83            user_agent().starts_with("recall-worker/"),
84            "{}",
85            user_agent()
86        );
87    }
88}