1use anyhow::Context;
27use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions};
28use std::path::Path;
29use std::sync::{Arc, OnceLock};
30
31pub(crate) mod control;
32pub(crate) mod dependencies;
33mod helpers;
34pub(crate) mod jobs;
35pub(crate) mod kv;
36mod metrics;
37mod pricing;
38pub(crate) mod replica;
39pub(crate) mod tasks;
40
41#[allow(unused_imports)]
42pub use control::{parse_since_duration, ChatCostSummary, ControlMessage, MemoryEntry};
43#[allow(unused_imports)]
44pub use dependencies::{
45 blocker_failure_kind, blocker_is_satisfied, blocker_snapshot_key, dep_label,
46 worktree_refresh_key, BlockerSnapshot, DepSource, NewDependency, ObservedPullRequest,
47 TaskDependency, WaitingOnDependency,
48};
49#[allow(unused_imports)]
50pub use helpers::{
51 get_cost_estimate, get_cost_estimate_result, get_recent_memory, get_recent_memory_result,
52 get_task_field, get_task_field_direct, get_token_usage, get_token_usage_result,
53 opt_store_get_task, opt_store_get_task_by_id, resolve_store_id, review_session_expected,
54 set_review_session_expected, store_increment, store_increment_by_id, store_log_activity,
55 store_reset_counters, store_reset_failure_counters, store_set, store_set_by_id,
56 store_set_result, store_set_result_by_id, store_touch_updated_at, StoreResult,
57};
58#[allow(unused_imports)]
59pub use jobs::JobState;
60#[allow(unused_imports)]
61pub use metrics::{
62 AgentStat, CostByGroup, CostPeriod, CostSummary, ErrorStat, HighReviewCycleTask,
63 InsertTaskMetric, MetricsSummary, SlowTaskInfo,
64};
65#[allow(unused_imports)]
66pub use pricing::{pricing_for_model, CostEstimate, ModelPricing, TokenUsage};
67pub use tasks::{
68 parse_cli_task_spec, CliTaskRef, CliTaskSpec, CompleteRun, NewTask, RunTokenUsage, StartRun,
69 StoreRoute, Task, TaskActivity, TaskRun, TaskStatus, UpsertExternal,
70};
71
72pub async fn default_db_path() -> anyhow::Result<std::path::PathBuf> {
74 crate::home::db_path().await
75}
76
77#[derive(Clone)]
79pub struct TaskStore {
80 pool: SqlitePool,
81 raft: Arc<OnceLock<crate::cluster::Node>>,
83 daemon: Arc<OnceLock<crate::cluster::local::Endpoint>>,
85}
86
87impl TaskStore {
88 pub async fn open(db_path: &Path) -> anyhow::Result<Self> {
92 let options = SqliteConnectOptions::new()
93 .filename(db_path)
94 .journal_mode(SqliteJournalMode::Wal)
95 .create_if_missing(true)
96 .busy_timeout(std::time::Duration::from_secs(5))
97 .pragma("foreign_keys", "ON");
98
99 let pool = SqlitePoolOptions::new()
100 .max_connections(5)
101 .connect_with(options)
102 .await
103 .with_context(|| format!("opening task store: {}", db_path.display()))?;
104
105 let store = Self {
106 pool,
107 raft: Arc::default(),
108 daemon: Arc::default(),
109 };
110 store.migrate().await?;
111 Ok(store)
112 }
113
114 #[allow(dead_code)]
116 pub async fn open_single(db_path: &Path) -> anyhow::Result<Self> {
121 let options = SqliteConnectOptions::new()
122 .filename(db_path)
123 .journal_mode(SqliteJournalMode::Wal)
124 .create_if_missing(true)
125 .busy_timeout(std::time::Duration::from_secs(5))
126 .pragma("foreign_keys", "ON");
127
128 let pool = SqlitePoolOptions::new()
129 .max_connections(1)
130 .connect_with(options)
131 .await
132 .with_context(|| format!("opening task store (single): {}", db_path.display()))?;
133
134 let store = Self {
135 pool,
136 raft: Arc::default(),
137 daemon: Arc::default(),
138 };
139 store.migrate().await?;
140 Ok(store)
141 }
142
143 #[cfg(test)]
145 pub async fn open_memory() -> anyhow::Result<Self> {
146 let options = SqliteConnectOptions::new()
147 .filename(":memory:")
148 .pragma("foreign_keys", "ON");
149 let pool = SqlitePoolOptions::new()
150 .max_connections(1)
151 .connect_with(options)
152 .await?;
153
154 let store = Self {
155 pool,
156 raft: Arc::default(),
157 daemon: Arc::default(),
158 };
159 store.migrate().await?;
160 Ok(store)
161 }
162
163 async fn migrate(&self) -> anyhow::Result<()> {
165 sqlx::migrate!("./migrations")
166 .run(&self.pool)
167 .await
168 .context("running task store migrations")?;
169 Ok(())
170 }
171
172 pub fn attach_node(&self, node: crate::cluster::Node) {
173 if self.raft.set(node).is_err() {
174 tracing::warn!("raft already attached to the task store");
175 }
176 }
177
178 pub fn is_leader(&self) -> bool {
181 self.raft.get().is_none_or(|node| node.is_leader())
182 }
183
184 pub(crate) fn replicates(&self) -> bool {
186 self.raft.get().is_some() || self.daemon.get().is_some()
187 }
188
189 pub fn node(&self) -> Option<&crate::cluster::Node> {
190 self.raft.get()
191 }
192
193 pub fn attach_daemon(&self, endpoint: crate::cluster::local::Endpoint) {
194 let _ = self.daemon.set(endpoint);
195 }
196
197 pub(crate) async fn write(
199 &self,
200 cmd: crate::cluster::Command,
201 ) -> anyhow::Result<crate::cluster::Reply> {
202 let Some(node) = self.raft.get() else {
203 if let Some(endpoint) = self.daemon.get() {
204 match crate::cluster::local::send(endpoint, &cmd).await {
205 Ok(reply) => return reply_result(reply),
206 Err(e) if e.is_connect() => {
208 tracing::debug!(err = %e, "daemon not listening, writing directly");
209 }
210 Err(e) => return Err(e.into()),
211 }
212 }
213
214 return self.apply_direct(cmd).await;
215 };
216 let req = crate::cluster::Request {
217 now: crate::cluster::now_stamp(),
218 cmd,
219 };
220 reply_result(node.write(req).await?)
221 }
222
223 pub(crate) async fn write_int(&self, cmd: crate::cluster::Command) -> anyhow::Result<i64> {
224 match self.write(cmd).await? {
225 crate::cluster::Reply::Int(n) => Ok(n),
226 other => anyhow::bail!("expected an integer reply, got {other:?}"),
227 }
228 }
229
230 pub(crate) async fn write_bool(&self, cmd: crate::cluster::Command) -> anyhow::Result<bool> {
231 match self.write(cmd).await? {
232 crate::cluster::Reply::Bool(b) => Ok(b),
233 other => anyhow::bail!("expected a boolean reply, got {other:?}"),
234 }
235 }
236
237 pub(crate) async fn apply_direct(
239 &self,
240 cmd: crate::cluster::Command,
241 ) -> anyhow::Result<crate::cluster::Reply> {
242 let mut tx = self.pool.begin().await?;
243 let reply = cmd
244 .apply(&mut tx, &crate::cluster::now_stamp(), false)
245 .await?;
246 tx.commit().await?;
247 Ok(reply)
248 }
249
250 #[allow(dead_code)]
252 pub fn pool(&self) -> &SqlitePool {
253 &self.pool
254 }
255}
256
257fn reply_result(reply: crate::cluster::Reply) -> anyhow::Result<crate::cluster::Reply> {
258 match reply {
259 crate::cluster::Reply::Err(e) => Err(anyhow::anyhow!(e)),
260 other => Ok(other),
261 }
262}
263
264#[cfg(test)]
265mod tests;