Skip to main content

sqlite_graphrag/commands/
daemon.rs

1use crate::constants::DAEMON_IDLE_SHUTDOWN_SECS;
2use crate::errors::AppError;
3use crate::output;
4use crate::paths::AppPaths;
5
6#[derive(clap::Args)]
7#[command(after_long_help = "EXAMPLES:\n  \
8    # Start the embedding daemon in the foreground (default 600s idle timeout)\n  \
9    sqlite-graphrag daemon\n\n  \
10    # Start with a longer idle timeout for batch ingestion\n  \
11    sqlite-graphrag daemon --idle-shutdown-secs 3600\n\n  \
12    # Health-check a running daemon (exit 4 if not running)\n  \
13    sqlite-graphrag daemon --ping\n\n  \
14    # Request graceful shutdown of a running daemon\n  \
15    sqlite-graphrag daemon --stop\n\n\
16AUTO-SPAWN BEHAVIOR:\n  \
17    recall and hybrid-search spawn a daemon automatically when none is running,\n  \
18    amortising model warm-up across multiple invocations (idle timeout 600s).\n\n  \
19    Disable per-invocation:  sqlite-graphrag recall \"query\" --autostart-daemon=false\n  \
20    Disable globally:        export SQLITE_GRAPHRAG_DAEMON_DISABLE_AUTOSTART=1\n\n  \
21    The --autostart-daemon flag takes precedence over the env var.")]
22pub struct DaemonArgs {
23    /// Idle timeout in seconds before the daemon auto-shuts down to release the embedding model.
24    /// Default 600s; raise for long-running batch ingestion to avoid cold-start overhead.
25    #[arg(long, default_value_t = DAEMON_IDLE_SHUTDOWN_SECS)]
26    pub idle_shutdown_secs: u64,
27    /// Send a health-check ping to a running daemon and exit. Returns NotFound (exit 4) if no daemon.
28    #[arg(long)]
29    pub ping: bool,
30    /// Request graceful shutdown of a running daemon. Returns NotFound (exit 4) if no daemon.
31    #[arg(long)]
32    pub stop: bool,
33    /// Timeout in seconds for graceful shutdown drain of active requests.
34    #[arg(
35        long,
36        env = "SQLITE_GRAPHRAG_SHUTDOWN_TIMEOUT_SECS",
37        default_value_t = 10
38    )]
39    pub shutdown_timeout_secs: u64,
40    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
41    pub json: bool,
42    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
43    pub db: Option<String>,
44}
45
46pub fn run(args: DaemonArgs) -> Result<(), AppError> {
47    let _ = args.json;
48    let paths = AppPaths::resolve(args.db.as_deref())?;
49    paths.ensure_dirs()?;
50
51    if args.ping {
52        let response = crate::daemon::try_ping(&paths.models)?
53            .ok_or_else(|| AppError::NotFound("daemon not running".to_string()))?;
54        if let crate::daemon::DaemonResponse::Ok { ref version, .. } = response {
55            if version != crate::constants::SQLITE_GRAPHRAG_VERSION {
56                tracing::warn!(target: "daemon_cmd",
57                    daemon_version = %version,
58                    cli_version = crate::constants::SQLITE_GRAPHRAG_VERSION,
59                    "daemon version mismatch; auto-restart will occur on the next embedding request"
60                );
61            }
62        }
63        output::emit_json(&response)?;
64        return Ok(());
65    }
66
67    if args.stop {
68        let response = crate::daemon::try_shutdown(&paths.models)?
69            .ok_or_else(|| AppError::NotFound("daemon not running".to_string()))?;
70        output::emit_json(&response)?;
71        return Ok(());
72    }
73
74    crate::daemon::run(
75        &paths.models,
76        args.idle_shutdown_secs,
77        args.shutdown_timeout_secs,
78    )
79}