Skip to main content

objectiveai_cli/command/daemon/
spawn.rs

1//! `daemon spawn` — launcher + resident foreground daemon.
2//!
3//! Launcher (`foreground` unset/false): the exact lock flow `api spawn`
4//! / `viewer spawn` use — `try_read` the lock, re-exec this cli as the
5//! foreground daemon if it isn't held, re-check on child exit.
6//!
7//! Foreground (`foreground:true`): the resident daemon. Under a blocking
8//! init gate it acquires the singleton lock, then launches every
9//! `daemon: true` plugin via the SHARED plugin executor
10//! (`plugins::run::execute`) as `<exec> daemon begin` — so each resident
11//! plugin gets the full bidirectional protocol (it can execute nested
12//! commands, exactly like `plugins run` and the conduit's `mcp begin`).
13//! The plugins are leashed to this process; if any exits, the whole
14//! daemon exits (and the OS releases the lock).
15
16use std::pin::Pin;
17
18use futures::{Stream, StreamExt};
19use objectiveai_sdk::cli::command::daemon::spawn::{Request, ResponseItem};
20use objectiveai_sdk::cli::command::plugins::run::{Path as RunPath, Request as RunRequest};
21
22use crate::context::Context;
23use crate::error::Error;
24
25type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
26
27pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
28    let foreground = request
29        .dangerous_advanced
30        .as_ref()
31        .and_then(|a| a.foreground)
32        .unwrap_or(false);
33    if foreground {
34        execute_foreground(ctx).await
35    } else {
36        // Non-foreground: the exact lock flow `api spawn` / `viewer
37        // spawn` use — try_read, exec the foreground daemon if not held,
38        // re-check on child exit.
39        spawn(ctx).await?;
40        Ok(Box::pin(futures::stream::once(async move {
41            Ok::<ResponseItem, Error>(ResponseItem { ok: true })
42        })))
43    }
44}
45
46/// Ensure the resident daemon is up, returning its published lock
47/// content. Mirrors [`crate::command::viewer::spawn::spawn`]: re-execs
48/// THIS cli as the foreground daemon via the shared
49/// `spawn_until_lock_published` helper.
50pub async fn spawn(ctx: &Context) -> Result<String, Error> {
51    let lock_dir = ctx.filesystem.state_dir().join("locks");
52    let exe = std::env::current_exe().map_err(|e| Error::Spawn("current_exe".into(), e))?;
53    crate::spawn::spawn_until_lock_published(
54        &exe,
55        &lock_dir,
56        super::DAEMON_LOCK_KEY,
57        |cmd| {
58            cmd.arg("daemon")
59                .arg("spawn")
60                .arg("--dangerous-advanced")
61                .arg("{\"foreground\":true}");
62            crate::spawn::apply_config_env(cmd, &ctx.config);
63        },
64    )
65    .await
66}
67
68/// Foreground: the resident daemon.
69async fn execute_foreground(ctx: &Context) -> Result<ItemStream, Error> {
70    let lock_dir = ctx.filesystem.state_dir().join("locks");
71    let lock_err = |e: std::io::Error| Error::Lockfile {
72        key: super::DAEMON_LOCK_KEY.to_string(),
73        source: e,
74    };
75
76    // First acquire the init gate (blocking), then the singleton lock.
77    let init = objectiveai_sdk::lockfile::wait_acquire(
78        &lock_dir,
79        super::DAEMON_INIT_LOCK_KEY,
80        "initializing",
81    )
82    .await
83    .map_err(lock_err)?;
84
85    let claim = match objectiveai_sdk::lockfile::try_acquire(
86        &lock_dir,
87        super::DAEMON_LOCK_KEY,
88        "ready",
89    )
90    .await
91    {
92        // A sibling daemon already holds the lock — bow out.
93        None => {
94            let _ = init.release();
95            return Ok(Box::pin(futures::stream::empty()));
96        }
97        Some(claim) => claim,
98    };
99    init.release().map_err(lock_err)?;
100
101    // Launch every daemon plugin under the SHARED plugin executor, run
102    // as `<exec> daemon begin`. `plugins::run::execute` spawns it leashed
103    // and drives the full nested-command protocol; we consume (drive) its
104    // stream below.
105    let manifests: Vec<crate::filesystem::plugins::Manifest> = ctx
106        .filesystem
107        .list_plugins(0, usize::MAX)
108        .await
109        .into_iter()
110        .filter(|m| m.daemon)
111        .collect();
112    let mut streams = Vec::new();
113    for manifest in manifests {
114        let request = RunRequest {
115            path_type: RunPath::PluginsRun,
116            owner: manifest.owner,
117            name: manifest.name,
118            version: manifest.version,
119            args: vec!["daemon".to_string(), "begin".to_string()],
120            base: Default::default(),
121        };
122        let stream = crate::command::plugins::run::execute(ctx, request).await?;
123        streams.push(stream);
124    }
125
126    let stream = async_stream::stream! {
127        // Hold the lock claim for the daemon's whole life: `LockClaim`
128        // never releases on drop (the OS reclaims the handles on process
129        // exit — exactly the liveness we want).
130        let _claim = claim;
131
132        let mut drains = futures::stream::FuturesUnordered::new();
133        for plugin_stream in streams {
134            drains.push(async move {
135                let mut plugin_stream = plugin_stream;
136                // Draining the stream DRIVES the plugin's protocol —
137                // nested commands run as items are consumed. The stream
138                // ends only when the plugin exits.
139                while plugin_stream.next().await.is_some() {}
140            });
141        }
142
143        // Ready: the launcher's handshake (and the lone item a direct
144        // `daemon spawn --foreground` watcher would see).
145        yield Ok::<ResponseItem, Error>(ResponseItem { ok: true });
146
147        if drains.is_empty() {
148            // No daemon plugins: stay resident so the singleton is held.
149            std::future::pending::<()>().await;
150        } else {
151            // Any plugin exiting ends the whole daemon.
152            let _ = drains.next().await;
153        }
154    };
155    Ok(Box::pin(stream))
156}
157
158pub mod request_schema {
159    use objectiveai_sdk::cli::command::daemon::spawn as sdk;
160    use objectiveai_sdk::cli::command::daemon::spawn::request_schema::{Request, Response};
161
162    use crate::context::Context;
163    use crate::error::Error;
164
165    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
166        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
167    }
168}
169
170pub mod response_schema {
171    use objectiveai_sdk::cli::command::daemon::spawn as sdk;
172    use objectiveai_sdk::cli::command::daemon::spawn::response_schema::{Request, Response};
173
174    use crate::context::Context;
175    use crate::error::Error;
176
177    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
178        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
179    }
180}