Skip to main content

sqry_daemon/
lib.rs

1//! `sqry-daemon` — long-lived code-graph service.
2//!
3//! The daemon (`sqryd` binary) owns one or more loaded code graphs in memory,
4//! watches source trees for changes, and serves CLI / LSP / MCP clients over a
5//! shared Unix-domain socket (named pipe on Windows). The goal is to amortise
6//! graph-load cost across every sqry invocation on a machine while preserving
7//! the semantic guarantees of direct-mode sqry (bijective per-file buckets,
8//! tombstone compaction, `ArcSwap` publish, etc.).
9//!
10//! # Architecture at a glance
11//!
12//! - [`config`] — parses `~/.config/sqry/daemon.toml` into a [`config::DaemonConfig`]
13//!   with every tuning knob from the Amendment-2 design (memory limits, working-set
14//!   multipliers, stale-serve age cap, debounce timing, interner compaction
15//!   threshold, log rotation, socket path).
16//! - [`workspace`] *(Task 6)* — `WorkspaceManager` owns `LoadedWorkspace` state
17//!   (§G admission accounting, §H rebuild plumbing, §F bijection check).
18//! - [`rebuild`] *(Task 7)* — per-workspace rebuild lane + coalescing (§J).
19//! - [`ipc`] *(Task 8)* — JSON-RPC over UDS with a standard response envelope.
20//! - [`lifecycle`] *(Task 9)* — pidfile locking, signal handling, service
21//!   unit generators.
22//! - [`client`] *(Task 10)* — client library used by `sqry-cli` /
23//!   `sqry-lsp --daemon` / `sqry-mcp --daemon` to connect to a running daemon
24//!   and auto-start one if necessary.
25//!
26//! Only [`config`] and the public error type [`DaemonError`] are in the surface
27//! today; later tasks in this plan land the other modules in order.
28
29pub mod config;
30/// Task 9 U10 — production sqryd binary entry point.
31///
32/// Owns the clap CLI (`SqrydCli`), the ordered startup / shutdown lifecycle
33/// (`run()`), and every `run_start` / `run_stop` / `run_status` /
34/// `run_install_*` / `run_print_config` dispatcher.  `main.rs` calls
35/// `sqry_daemon::entrypoint::main_impl()` which parses the CLI, builds the
36/// tokio runtime, and maps every error to a POSIX `sysexits.h` exit code via
37/// `DaemonError::exit_code()`.
38pub mod entrypoint;
39pub mod error;
40pub mod ipc;
41/// Task 9 — daemon binary lifecycle: pidfile locking, signal handling, service
42/// unit generators, log rotation, and auto-spawn primitives.
43///
44/// The module is built up incrementally across Task 9 units. Only the units
45/// that have landed so far are present; later units (U3–U10) add submodules as
46/// they are implemented.
47pub mod lifecycle;
48/// Phase 8c U8 — in-daemon MCP host.
49///
50/// Hosts an rmcp `ServerHandler` in-process for each MCP shim
51/// byte-pump connection (see [`mcp_host::host_mcp_on_streams`]),
52/// routing every `tools/call` through Phase 8b's
53/// `daemon_adapter::execute_*_for_daemon` path via the shared
54/// [`ipc::tool_core::classify_and_execute`] pipeline. MCP tool
55/// behaviour is bit-identical to direct sqryd JSON-RPC tool dispatch.
56pub mod mcp_host;
57pub mod rebuild;
58pub mod workspace;
59
60pub use config::{
61    DEFAULT_IPC_SHUTDOWN_DRAIN_SECS, DaemonConfig, ESTIMATE_FINAL_PER_FILE_BYTES,
62    ESTIMATE_STAGING_PER_FILE_BYTES, INTERNER_BUILDER_OVERHEAD_RATIO, SocketConfig,
63    WORKING_SET_MULTIPLIER, WorkspaceConfig,
64};
65pub use error::{DaemonError, DaemonResult};
66pub use ipc::{
67    CancelRebuildResult, DaemonHello, DaemonHelloResponse, IpcServer, JsonRpcError, JsonRpcId,
68    JsonRpcPayload, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, RebuildResult,
69    ResponseEnvelope, ResponseMeta,
70};
71pub use rebuild::{
72    CapturedIteration, RebuildDispatcher, RebuildMode, TestCapture, TestGate, decide_mode,
73};
74pub use workspace::{
75    BACKOFF_SCHEDULE, DaemonStatus, EmptyGraphBuilder, FailingGraphBuilder, LoadedWorkspace,
76    MemoryStatus, NoOpHook, OldGraphToken, PendingRebuild, RealWorkspaceBuilder,
77    RebuildReservation, ServeVerdict, SharedHook, SqrydHook, StalenessVerdict, WorkingSetInputs,
78    WorkspaceBuilder, WorkspaceKey, WorkspaceManager, WorkspaceState, WorkspaceStatus,
79    backoff_delay_for, classify_staleness, noop_hook, spawn_hook, working_set_estimate,
80};
81
82/// JSON-RPC error code: per-tool invocation exceeded
83/// `DaemonConfig::tool_timeout_secs`. Emitted by
84/// `tool_core::classify_and_execute` (Task 8 Phase 8c U6) when the
85/// `tokio::time::timeout(tool_timeout, spawn_blocking(run))` outer
86/// timer fires. The detached `JoinHandle` is dropped — the OS thread
87/// may continue executing the tool closure but its result is
88/// discarded.
89///
90/// Source: Task 8 Phase 8c design §O (iter-2 Codex-approved wire contract).
91pub const JSONRPC_TOOL_TIMEOUT: i32 = -32000;
92
93/// JSON-RPC error code: workspace build failed and no prior good graph exists.
94///
95/// Source: Amendment 1 §C, Amendment 2 §G.7.
96pub const JSONRPC_WORKSPACE_BUILD_FAILED: i32 = -32001;
97
98/// JSON-RPC error code: the workspace is serving a Failed state, but the last
99/// successful build is older than `stale_serve_max_age_hours`.
100///
101/// Source: Amendment 1 §C.
102pub const JSONRPC_WORKSPACE_STALE_EXPIRED: i32 = -32002;
103
104/// JSON-RPC error code: admission control could not satisfy a reservation
105/// after evicting every non-pinned workspace.
106///
107/// Source: Amendment 2 §G.1, §G.7.
108pub const JSONRPC_MEMORY_BUDGET_EXCEEDED: i32 = -32003;
109
110/// JSON-RPC error code: the workspace was evicted or removed between a
111/// rebuild dispatch and its admission / publish commit. Callers must treat
112/// this as a terminal signal on the affected `WorkspaceKey` — subsequent
113/// dispatches require a fresh `get_or_load` first.
114///
115/// Source: Amendment 2 §J (same-workspace rebuild serialization), Task 7
116/// Phase 7b1 (runner-role gate + `reserve_rebuild` eviction check).
117///
118/// # Daemon public JSON-RPC error codes (authoritative table)
119///
120/// | Code    | Variant                | Semantics                                                      |
121/// |---------|------------------------|----------------------------------------------------------------|
122/// | -32000  | `ToolTimeout`          | Per-tool `tool_timeout_secs` deadline elapsed (Phase 8c U6).   |
123/// | -32001  | `WorkspaceBuildFailed` | Build failed, no prior good graph.                             |
124/// | -32002  | `WorkspaceStaleExpired`| Stale-serve window exceeded `stale_serve_max_age_hours`.       |
125/// | -32003  | `MemoryBudgetExceeded` | Admission cannot fit even after evicting all non-pinned.       |
126/// | -32004  | `WorkspaceEvicted`     | Workspace gone mid-rebuild; caller must re-`get_or_load`.      |
127/// | -32005  | `WorkspaceIncompatibleGraph` | On-disk graph cannot be used by this binary (plugin or format mismatch). |
128/// | -32602  | `InvalidArgument`      | Tool-argument validation failure (JSON-RPC standard).          |
129/// | -32603  | `Internal`             | Catch-all bubbled from `sqry_mcp::daemon_adapter` execution.   |
130/// | n/a     | `AlreadyRunning`       | Another sqryd holds the pidfile lock (Task 9 U1). Exit 75.    |
131/// | n/a     | `AutoStartTimeout`     | `start_detached` socket poll timed out (Task 9 U1). Exit 69.  |
132/// | n/a     | `SignalSetup`          | `tokio::signal` handler install failed (Task 9 U1). Exit 70.  |
133pub const JSONRPC_WORKSPACE_EVICTED: i32 = -32004;
134
135/// JSON-RPC error code: the on-disk graph snapshot or manifest cannot be
136/// loaded safely by this binary. Distinct from `WorkspaceBuildFailed`
137/// because it represents a path-policy / compatibility verdict (unknown
138/// plugin ids, unsupported snapshot format) rather than a transient
139/// build failure — clients react differently (rebuild vs. upgrade vs.
140/// retry).
141///
142/// SGA02 / SGA04 acceptance: "API carries path-policy errors distinctly
143/// from load, stale, eviction, and corruption errors" — adapters must
144/// not collapse this taxonomy class into the generic build-failed code.
145pub const JSONRPC_WORKSPACE_INCOMPATIBLE_GRAPH: i32 = -32005;
146
147/// JSON-RPC error code: the freshly-built graph exceeds the daemon's
148/// memory budget *by itself* (post-build oversize) — even after every
149/// other workspace would be evicted, the daemon cannot host this
150/// graph. Distinct from `MemoryBudgetExceeded` (`-32003`), which is a
151/// *projected* admission failure on a pre-build estimate.
152///
153/// Source: `G_daemon_control_plane.md` §1.4 (post-build heap check) +
154/// `00_contracts.md` §3.CC-3 (admission boundary with DPA / DPC).
155/// Returned by `WorkspaceManager::publish_and_retain` after the build
156/// completes but before the new graph is exposed to readers.
157pub const JSONRPC_WORKSPACE_OVERSIZE: i32 = -32006;
158
159/// JSON-RPC error code: socket parent directory cannot be created or
160/// is not writable by the daemon's uid. Surfaced before `IpcServer::bind`
161/// so the failure mode is a precise diagnostic instead of a generic
162/// `EACCES` from the eventual bind.
163///
164/// Source: `G_daemon_control_plane.md` §5.2.
165pub const JSONRPC_SOCKET_SETUP: i32 = -32007;
166
167/// JSON-RPC error code: `daemon/reset` was invoked on a workspace
168/// whose state is `Loading` and cannot be safely interrupted yet.
169/// Caller should retry once the load completes.
170///
171/// Source: `G_daemon_control_plane.md` §3.2.
172pub const JSONRPC_RESET_WHILE_LOADING: i32 = -32008;
173
174/// JSON-RPC error code: `daemon/reset` was invoked on a workspace
175/// whose state is `Rebuilding`; cancellation has been dispatched and
176/// the caller is expected to retry after `retry_after_ms` for the
177/// state to settle into `Failed` / `Unloaded` (then a follow-up reset
178/// completes).
179///
180/// Source: `G_daemon_control_plane.md` §3.2.
181pub const JSONRPC_RESET_CANCELLATION_DISPATCHED: i32 = -32009;
182
183/// JSON-RPC error code: `daemon/reset` refused because the targeted
184/// workspace is pinned and the caller did not pass `force = true`.
185/// Pinning is a per-workspace operator override; callers must opt in
186/// explicitly to drop a pinned workspace.
187///
188/// Source: `G_daemon_control_plane.md` §3.2.
189pub const JSONRPC_WORKSPACE_PINNED: i32 = -32010;
190
191/// JSON-RPC error code: pre-flight cost gate rejected a query because
192/// its evaluator cost is structurally unbounded (no scope filter, no
193/// regex anchoring, predicate shape would scan the full arena). Wire
194/// `kind` is always `"query_too_broad"`. Reuses `-32602` per the
195/// existing wire-bridge convention; `kind` is the discriminator
196/// (per `B_cost_gate.md` §3 "Why -32602, not a new -32xxx code").
197///
198/// Source: `B_cost_gate.md` §3 + `00_contracts.md` §3.CC-2.
199pub const JSONRPC_QUERY_TOO_BROAD: i32 = JSONRPC_INVALID_PARAMS;
200
201/// JSON-RPC 2.0 standard "Invalid params" error code.
202///
203/// Surfaced by `tool_core` argument validation (Phase 8c U6) BEFORE
204/// workspace classification runs — e.g. `resolve_index_root` failures
205/// and missing `path` arguments in MCP tool args.
206pub const JSONRPC_INVALID_PARAMS: i32 = -32602;
207
208/// JSON-RPC 2.0 standard "Internal error" code. Catch-all for errors
209/// bubbling from `sqry_mcp::daemon_adapter` tool execution that don't
210/// map to a more specific `DaemonError` variant.
211pub const JSONRPC_INTERNAL_ERROR: i32 = -32603;
212
213/// Version of the daemon wire envelope (`DaemonHelloResponse.envelope_version`).
214///
215/// Re-exported from `sqry-daemon-protocol` so callers that only depend on
216/// `sqry-daemon` (or on `sqry-daemon-client`) both see the same single source
217/// of truth. See [`sqry_daemon_protocol::ENVELOPE_VERSION`] for the canonical
218/// definition and bump policy.
219pub use sqry_daemon_protocol::ENVELOPE_VERSION;
220
221// ---------------------------------------------------------------------------
222// SGA07 parity test hooks (test-only re-exports)
223// ---------------------------------------------------------------------------
224
225/// SGA07 parity test hook — snapshot the process-wide counter that the
226/// daemon graph provider bumps on every [`acquire`](workspace::acquirer)
227/// call. Returns the current count without resetting it. Gated on
228/// `#[cfg(any(test, feature = "test-hooks"))]` so the symbol is
229/// unreachable in default release builds.
230#[cfg(any(test, feature = "test-hooks"))]
231#[doc(hidden)]
232pub fn acquire_counter_snapshot() -> usize {
233    workspace::acquirer::acquire_counter_snapshot()
234}
235
236/// SGA07 parity test hook — reset the process-wide acquisition counter
237/// to zero. Returns the previous value so callers can sanity-check a
238/// reset between dispatches. Production code MUST NOT call this.
239#[cfg(any(test, feature = "test-hooks"))]
240#[doc(hidden)]
241pub fn acquire_counter_reset() -> usize {
242    workspace::acquirer::acquire_counter_reset()
243}
244
245// ---------------------------------------------------------------------------
246// Shared test-only ENV_LOCK
247// ---------------------------------------------------------------------------
248
249/// Single process-wide mutex for tests that manipulate `XDG_RUNTIME_DIR`.
250///
251/// Multiple test modules (`pidfile`, `detach`, `config`) run as threads in the
252/// same binary.  Each module previously had its own `ENV_LOCK`, which allowed
253/// concurrent `XDG_RUNTIME_DIR` mutations and produced flaky pidfile-PID
254/// mismatches.  This shared lock serialises all env-var mutations across every
255/// `#[cfg(test)]` module in the crate.
256#[cfg(test)]
257pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());