1 //! Core engine module for `DeepSeek` CLI.
2 //!
3 //! This module provides the event-driven architecture that separates
4 //! the UI from the AI interaction logic:
5 //!
6 //! - `engine`: The main engine that processes operations
7 //! - `events`: Events emitted by the engine to the UI
8 //! - `ops`: Operations submitted by the UI to the engine
9 //! - `session`: Session state management
10 //! - `turn`: Turn context and tracking
11
12 // Engine code runs inside the TUI alt-screen — see `runtime_log` for why
13 // raw stdio prints must not appear here. Use `tracing::*` instead.
14 #![deny(clippy::print_stdout)]
15 #![deny(clippy::print_stderr)]
1920 /// Run the engine event loop
1921 #[allow(clippy::too_many_lines)]
1922 pub async fn run(mut self) {
1923 // RuntimeThreadManager owns durable turn claims and installs a thread
1924 // id in runtime services. Only the interactive TUI may autonomously
1925 // create a new turn while the engine is otherwise idle; a hosted
1926 // engine must wait for its host to claim and explicitly dispatch the
1927 // next turn so events cannot be attached to the wrong durable record.
1928 let host_managed_turns = self.host_managed_turns();
1929
1930 loop {
1931 let Some(input) = self.next_run_input(host_managed_turns).await else {
1932 break;
1933 };
1934
1935 // Runtime posture updates publish through shared typed state
1936 // before attempting their best-effort wake-up. If the mailbox was
1937 // already full, its next queued operation is the wake-up: apply
1938 // the latest authority before doing any work under an obsolete
1939 // policy.
1940 if matches!(&input, EngineRunInput::Operation(_)) {
1941 self.apply_pending_runtime_authority().await;
1942 }
3385 /// Build the turn's tool registry and the model-facing tool catalog.
3386 ///
3387 /// This is the single authority for "what tools would the next request
3388 /// carry". `handle_send_message` calls it with [`SubAgentWiring::Live`]
3389 /// and [`McpAccess::Connect`]; `/preview-request` calls it with
3390 /// [`SubAgentWiring::Inert`] and [`McpAccess::PassiveSnapshot`], which
3391 /// together remove every side effect of the build — no fork snapshot, no
3392 /// spawned mailbox drainer, no pool creation, no `connect_all`, no status
3393 /// events — while producing a byte-identical catalog for the state that
3394 /// is already live.
3395 ///
3396 /// The session's `last_tool_catalog` is never an acceptable substitute:
3397 /// it is one turn stale and stores the pre-activation catalog rather than
3398 /// the active subset the provider would actually receive.
3399 ///
3400 /// `allowed_tools` is the command-scoped allow-list gate the catalog is
3401 /// filtered under. It is an explicit **parameter**, not a read of
3402 /// `self.config.allowed_tools`, because the preview's gate belongs to a
3403 /// turn that has not been installed: writing it onto the engine and
3404 /// restoring it afterwards would leave the wrong gate installed across
3405 /// every `.await` in this function, and would leave it installed
3406 /// permanently if the task were cancelled or panicked between the two
3407 /// writes.
3408 #[allow(clippy::too_many_arguments)]
3409 async fn build_turn_tool_registry_and_catalog(
3410 &mut self,
3411 input_policy: &TurnAuthority,
3412 dynamic_tools: &[DynamicToolSpec],
3413 allowed_tools: Option<Vec<String>>,
3414 wiring: SubAgentWiring,
3415 mcp_access: McpAccess,
3416 route: TurnRouteContext,
3417 turn_id: &str,
3418 ) -> TurnToolBuild {
364 pub(super) async fn handle_deepseek_turn(
365 &mut self,
366 turn: &mut TurnContext,
367 tool_policy: ToolSurfacePolicy,
368 // Out-of-request facts resolved once for this turn. `None` means the
369 // caller captured none, and the projection reports every
370 // registry-derived field as unknown rather than guessing.
371 inspection_surface: Option<crate::tool_inspection::ToolSurfaceContext>,
372 ) -> (TurnOutcomeStatus, Option<String>) {
373 // Only interactive TUI hosts own terminal chrome. Headless exec,
374 // app-server, and stream-json stdout must remain byte-clean.
375 if self.config.terminal_chrome_enabled {
376 crate::tui::notifications::set_taskbar_progress_busy();
377 crate::tui::notifications::start_title_animation("Codewhale");
378 }
379
380 let client = self
381 .model_client
382 .clone()
383 .expect("model client should be configured");
384
385 let mut consecutive_tool_error_steps = 0u32;
386 let mut stuck_guard = StuckGuard::default();
387 // Scoped to this external user turn: counts survive all model/tool
388 // steps below, then reset before the next user prompt.
… 13 lines omitted; exact range 364–412 …
402 // (no declared budget) leaves the gate below inert.
403 let mut tool_call_budget = ToolCallBudget::new(tool_policy.max_tool_calls);
404 let mut goal_continuations_this_turn = 0u32;
405 // Outer stream-retry counter: when the chunked-transfer connection
406 // dies mid-stream and either nothing useful was streamed (#103
407 // Phase 3) or the host slept mid-turn (#2990), we silently re-issue
408 // the SAME request up to MAX_STREAM_RETRIES times before surfacing
409 // the failure to the user.
410 let mut stream_retry_attempts: u32 = 0;
411
412 loop {
620 // Build the request. Tool selection goes through the same
621 // helper that seeded this turn and that `/preview-request`
622 // reports, so a deferred tool activated mid-turn is reflected
623 // identically in both places.
624 let active_tools =
625 active_tools_for_request(&tool_catalog, &active_tool_names, strict_tool_mode);
626
627 // Resolve `auto` reasoning_effort to a concrete tier (#663).
628 let effective_reasoning_effort = resolve_auto_effort(
629 self.session.reasoning_effort.as_deref(),
630 &self.session.messages,
631 self.api_provider,
632 &self.api_config.deepseek_base_url(),
633 &self.config.model,
634 );
635
636 // Check prefix-cache stability before building the request.
637 // This detects system-prompt or tool-set drift that would
638 // invalidate DeepSeek's KV prefix cache for this turn.
639 // Sends an event on EVERY check so the TUI can maintain
640 // its own counter for the stable-checks tally.
641 if let Some(pm) = self.session.prefix_stability.as_mut() {
642 let system_text =
643 crate::prefix_cache::system_prompt_text(self.session.system_prompt.as_ref());
644 let tools_ref: Option<&[crate::models::Tool]> = active_tools.as_deref();
… 32 lines omitted; exact range 620–687 …
677 description: String::new(),
678 system_prompt_changed: false,
679 tools_changed: false,
680 stability_pct: (pm.stability_ratio() * 100.0).round() as u32,
681 changed: false,
682 pinned_combined_hash: pinned_hash,
683 })
684 .await;
685 }
686 }
687 }
1 //! Cross-process admission for expensive local commands.
2 //!
3 //! Fleet and Workflow workers execute in separate Codewhale processes, so an
4 //! in-process semaphore cannot protect the host. Heavy shell commands instead
5 //! take one of a small number of filesystem-backed permits under
6 //! `CODEWHALE_HOME`. The default of two permits is deliberately conservative
7 //! for the 36 GiB laptop class from #4864.
8
9 use std::fs::{File, OpenOptions};
10 use std::io;
11 use std::path::{Path, PathBuf};
12 use std::time::{Duration, Instant};
13
14 use anyhow::{Context, Result, anyhow};
15 use fd_lock::{RwLock, RwLockWriteGuard};
16 use tokio_util::sync::CancellationToken;
17
18 pub(crate) const DEFAULT_HEAVY_COMMAND_LIMIT: usize = 2;
19 const MAX_HEAVY_COMMAND_LIMIT: usize = 16;
20 const ADMISSION_POLL_INTERVAL: Duration = Duration::from_millis(50);
21
22 /// When the host free-RAM fraction drops to/below these thresholds the
23 /// effective heavy-command admission limit tightens so a saturated host stops
24 /// admitting new link graphs (#4864 req 7). Values are deliberately generous
25 /// because the measurement is advisory, not authoritative.
26 const CONSTRAINED_FREE_FRACTION: f64 = 0.30;
27 const CRITICAL_FREE_FRACTION: f64 = 0.15;
1 //! Unified context-budget math for the TUI.
2 //!
3 //! Given a model's context window, the current input token estimate, and a
4 //! configured output cap, [`ContextBudget`] derives the four numbers the rest
5 //! of the app needs to reason about a turn:
6 //!
7 //! * **available input budget** — how many input tokens may still be spent
8 //! after reserving room for the model's output;
9 //! * **output token cap** — the output reservation actually used to compute
10 //! that budget (clamped so it never starves the window);
11 //! * **compaction trigger** — the input-token level at which compaction
12 //! should be suggested (default: ~75% of the spendable input ceiling);
13 //! * **[`PressureLevel`]** — a coarse Low/Medium/High/Critical signal the UI
14 //! can render without re-deriving thresholds.
15 //!
16 //! This module is the budget-math *foundation*. It is intentionally pure (no
17 //! I/O, no clock, no engine/config types) so it can be unit-tested in isolation
18 //! and later consumed by the engine capacity checkpoints and the TUI pressure
19 //! indicator. Those consumers are wired in a separate pass; nothing here calls
20 //! into them.
21 //!
22 //! ### Why the output reservation is window-dependent
23 //!
24 //! The engine's existing input-budget helper
25 //! (`core::engine::context::context_input_budget_for_window`) computes
26 //! `window - reserved_output - headroom` and learned the hard way that
27 //! reserving a large fixed output (262K for V4-class interleaved thinking) on a
28 //! *small* self-hosted window (e.g. a 256K vLLM deployment) underflows to a
29 //! negative budget and silently disables every preflight/recovery path. We
30 //! mirror that lesson here with saturating arithmetic and an output cap that is
31 //! always clamped to leave at least [`MIN_INPUT_BUDGET_TOKENS`] of input room,
32 //! so the budget can never collapse to zero on a legitimately sized window.
200 ///
201 /// Output is sorted by tool name for **prefix-cache stability** (#263).
202 /// Rust's `HashMap` uses a randomly-seeded hasher per process, so a raw
203 /// `self.tools.values()` iteration emits tools in a different order on
204 /// every `deepseek` launch, invalidating DeepSeek's KV prefix cache for
205 /// every cross-session resume. Sorting here matches the way Claude Code
206 /// stabilises its tool array (`assembleToolPool` in their reference).
207 ///
208 /// The serialised catalog is memoised on first call and pinned across
209 /// reads so each tool's `description()` and `input_schema()` are sampled
210 /// exactly once per registration. MCP adapters whose upstream description
211 /// drifts on reconnect would otherwise rewrite the catalog mid-session
212 /// and bust the prefix cache. The cache is invalidated on `register`,
213 /// `remove`, and `clear`.
214 #[must_use]
215 pub fn to_api_tools(&self) -> Vec<Tool> {
216 self.api_cache
217 .get_or_init(|| self.build_api_tools())
218 .clone()
219 }
220
221 fn build_api_tools(&self) -> Vec<Tool> {
222 let mut tools: Vec<&Arc<dyn ToolSpec>> = self.tools.values().collect();
223 tools.sort_by(|a, b| a.name().cmp(b.name()));
224 tools
… 9 lines omitted; exact range 200–244 …
234 description: tool.description().to_string(),
235 input_schema: schema,
236 allowed_callers: Some(vec!["direct".to_string()]),
237 defer_loading: Some(tool.defer_loading()),
238 input_examples: None,
239 strict: None,
240 cache_control: None,
241 }
242 })
243 .collect()
244 }
1 //! Goal loop orchestrator — the persistent-objective control layer (#3215, and
2 //! its lineage #891 / #1976 / #2058 / #2029).
3 //!
4 //! This is the **Workflow goal layer**: the decision core that turns a one-shot
5 //! `/goal` into a persistent work loop. Given the durable goal status, the
6 //! accumulated usage (from the per-goal accounting wired in `crates/state`
7 //! `record_thread_goal_usage`), and a budget, it decides whether to **continue**
8 //! (re-dispatch another worker turn toward the objective) or **stop** with a
9 //! terminal status. It is the orchestrator in the Workflow≈ultracode mapping —
10 //! the loop that fans work out to workers (`worker_profile`) and verifies before
11 //! committing.
12 //!
13 //! Scope: **decision logic + types**. The engine (`core/engine.rs`) reads the
14 //! `SharedGoalState` snapshot after each turn and calls `decide_continuation`
15 //! to decide whether to re-dispatch. A small cross-turn circuit breaker keeps
16 //! an unbounded goal from silently spending forever when the model never emits
17 //! a terminal signal; explicit token/time budgets still take precedence.
18
19 /// Maximum automatic cross-turn continuation passes for one goal.
20 ///
21 /// This matches the conservative run-cap used by the peer goal lifecycle while
22 /// avoiding its much larger classifier/strategist subsystem.
23 pub const MAX_GOAL_CONTINUATIONS: u32 = 10;
17 /// Determines execution restrictions for shell commands.
18 ///
19 /// The sandbox policy controls filesystem access, network access, and other
20 /// system resources for executed commands. Choose the most restrictive policy
21 /// that still allows your command to function.
22 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23 #[serde(tag = "type", rename_all = "kebab-case")]
24 pub enum SandboxPolicy {
25 /// No restrictions whatsoever. Use with extreme caution.
26 ///
27 /// This policy disables all sandboxing and allows full system access.
28 /// Only use this when absolutely necessary and the command source is trusted.
29 #[serde(rename = "danger-full-access")]
30 DangerFullAccess,
31
32 /// Read-only access to the entire filesystem.
33 ///
34 /// The process can read any file but cannot write anywhere.
35 /// Useful for analysis tools that need broad read access.
36 #[serde(rename = "read-only")]
37 ReadOnly,
38
39 /// Indicates the process is already running in an external sandbox.
40 ///
41 /// Use this when CodeWhale is itself running inside a container,
… 35 lines omitted; exact range 17–87 …
77 impl Default for SandboxPolicy {
78 /// Returns the default policy: workspace-write with no extra roots and no network.
79 fn default() -> Self {
80 SandboxPolicy::WorkspaceWrite {
81 writable_roots: vec![],
82 network_access: false,
83 exclude_tmpdir: false,
84 exclude_slash_tmp: false,
85 }
86 }
87 }
3 //! Sandbox module for secure command execution.
4 //!
5 //! This module provides sandboxing capabilities for shell commands executed by
6 //! CodeWhale. Sandboxing restricts what system resources a command can access,
7 //! preventing accidental or malicious damage to the system.
8 //!
9 //! # Platform Support
10 //!
11 //! - **macOS**: Uses Seatbelt (`sandbox-exec`) when the runtime probe succeeds
12 //! - **Linux**: Uses bubblewrap only when the user opts in and `/usr/bin/bwrap`
13 //! is executable. Landlock and seccomp helpers are not wired into child
14 //! execution yet and therefore are not advertised.
15 //! - **OpenHarmony**: No local Linux sandbox is advertised. Bubblewrap,
16 //! Landlock, seccomp, and Linux `prctl` hardening are gated out under
17 //! `target_env = "ohos"`.
18 //! - **Windows**: No OS sandbox is advertised yet. The planned first helper
19 //! contract is process-tree containment only via a Windows Job Object; it
20 //! must not claim filesystem, network, registry, or AppContainer isolation.
1 //! The one place that decides whether a delegated child may make a call that
2 //! **executes**, **mutates**, or **reaches the network**.
3 //!
4 //! Before this module the answer was spread across three hand-maintained name
5 //! lists ([`crate::fleet::exact::RAW_SHELL_DENYLIST`] and its siblings) plus a
6 //! role posture that keyed on `ShellPolicy::Full`. That shape had a structural
7 //! hole: a name list can only deny the execution primitives someone remembered
8 //! to write down, and `shell = "full"` was being read as "may run arbitrary
9 //! code" by every tool whose approval requirement is `Required`. So a member
10 //! saved as read-only-with-checks (`write = false`, `shell = "full"` — the
11 //! `tester`/`verifier` preset, and any `custom` member shaped like it) lost
12 //! `Bash` and kept:
13 //!
14 //! - `tasks{action:"gate_run"}` — runs an operator-supplied command line;
15 //! - `automation{action:"run"}` / `{action:"create"}` — executes or schedules a
16 //! stored automation, with its own cwd and prompt;
17 //! - `start_mcp_server` — spawns a process and opens a socket;
18 //! - every repository plugin tool, which is a shell command by definition;
19 //!
20 //! each of which mutates the workspace and reaches the network exactly as well
21 //! as the shell that was just removed, while the receipt said `write=false`.
22 //!
23 //! ## What is enforced
24 //!
25 //! The classification is derived, never listed: it comes from the tool's own
… 23 lines omitted; exact range 1–59 …
49 //! whole purpose of a read-only verifier, and the shipped `verifier` role is
50 //! exactly `write = false, shell = "full"`. Classifying it by tool name would
51 //! either take the role's job away or hand it a program launcher, so the
52 //! bound is read off the concrete call by [`classify_verification`]:
53 //! argument-free and pure test *selection* both cost shell authority (each
54 //! forks a process, which `analyst`/`scout` were never granted), and
55 //! anything that can name a program is held to the raw-shell bar. Every
56 //! consumer of that contract — the catalog filter, the dispatch guard, and
57 //! `reject_unbounded_verification` / `is_delegated_builtin_verification` in
58 //! [`crate::tools::subagent`] — reads this one classifier rather than
59 //! re-deriving it.
1 //! Async MCP (Model Context Protocol) Implementation
2 //!
3 //! This module provides full async support for MCP servers with:
4 //! - Connection pooling for server reuse
5 //! - Automatic tool discovery via `tools/list`
6 //! - Configurable timeouts per-server and globally
7
8 use std::collections::{HashMap, HashSet};
9 use std::ffi::{OsStr, OsString};
10 use std::fs;
11 use std::future::Future;
12 use std::io::{Read, Seek};
13 use std::path::{Component, Path, PathBuf};
14 use std::sync::Arc;
15 use std::sync::atomic::{AtomicU64, Ordering};
16 use std::time::Duration;
17
18 use anyhow::{Context, Result};
19 use parking_lot::RwLock;
20 use serde::{Deserialize, Serialize};
21 use sha2::Digest as _;
22
23 pub mod external_import;
24 mod headers;
25 pub mod oauth;
… 1 lines omitted; exact range 1–37 …
27 mod stdio;
28 mod streamable_http;
29
30 use self::headers::{apply_safe_custom_headers, with_default_mcp_http_headers};
31 use self::sse::SseTransport;
32 use self::stdio::StdioTransport;
33 #[cfg(all(test, unix))]
34 use self::stdio::{STDIO_SHUTDOWN_GRACE, StderrTail};
35 use self::streamable_http::{StreamableHttpTransport, StreamableSendError};
36 use crate::network_policy::{Decision, NetworkPolicyDecider, host_from_url};
37 use crate::utils::write_atomic;
57 /// Expand `${NAME}` placeholders in an MCP config value from the process
58 /// environment. This lets secrets (API keys, bearer tokens, …) be supplied
59 /// through environment variables instead of being written in cleartext into
60 /// the MCP config file on disk.
61 ///
62 /// On a missing or malformed placeholder the error names only the offending
63 /// variable, never the surrounding value, so a secret-bearing string is never
64 /// echoed into logs or error output.
65 fn expand_env_placeholders_with(
66 value: &str,
67 environment: Option<&crate::plugins::HostEnvironment>,
68 ) -> Result<String> {
69 let mut out = String::new();
70 let mut rest = value;
71 while let Some(start) = rest.find("${") {
72 out.push_str(&rest[..start]);
73 let after = &rest[start + 2..];
74 let Some(end) = after.find('}') else {
75 anyhow::bail!("unterminated environment placeholder in MCP config value");
76 };
77 let name = &after[..end];
78 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
79 anyhow::bail!("invalid environment placeholder in MCP config value");
80 }
81 let env_value = environment
82 .map_or_else(|| std::env::var(name), |env| env.var(name))
83 .with_context(|| {
84 format!("environment variable {name} required by MCP config is not set")
85 })?;
86 out.push_str(&env_value);
87 rest = &after[end + 1..];
88 }
89 out.push_str(rest);
90 Ok(out)
1 //! Sub-agent spawning system.
2 //!
3 //! Provides tools to spawn background sub-agents, query their status,
4 //! and retrieve results. Sub-agents run with a filtered toolset and
5 //! inherit the workspace configuration from the main session.
6 //!
7 //! The model-facing creation surface is the `agent` tool. Narrow coordination
8 //! tools (`agents/list`, `agents/message`, `agents/followup`,
9 //! `agents/interrupt`, `agents/coordinate`, `agents/wait`) wrap the same runtime without restoring
10 //! the retired lifecycle theater. Older manager helpers remain executable for
11 //! persisted records and internal recovery.
1 //! Mailbox abstraction for sub-agent runtime coordination.
2 //!
3 //! Monotonic sequence numbers give every consumer a consistent ordering even
4 //! when multiple subscribers (e.g. UI card + parent agent) drain
5 //! independently; close-as-cancel lets a single signal both stop new mail and
6 //! propagate cancellation through nested children.
7
8 use std::collections::VecDeque;
9 use std::sync::Arc;
10 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11 #[cfg(test)]
12 use std::time::Duration;
13
14 use serde::{Deserialize, Serialize};
15 use tokio::sync::{mpsc, watch};
16 use tokio_util::sync::CancellationToken;
17
18 #[cfg(test)]
19 use crate::config::ApiProvider;
20 use crate::models::Usage;
21 use crate::tools::todo::TodoListSnapshot;
22
23 use super::FleetRole;
24
25 /// Stable, structured progress envelope shared across the sub-agent surface.
… 56 lines omitted; exact range 1–92 …
82 agent_id: String,
83 /// Stable identity of the provider response. Runtime accounting uses
84 /// this across direct durability, mailbox replay, and restart dedupe.
85 source_id: String,
86 /// Immutable provider/model/billing evidence captured before the
87 /// child request was sent.
88 route: crate::cost_status::EffectiveRouteEnvelope,
89 /// Provider usage payload, including cache-hit/cache-miss fields.
90 usage: Usage,
91 },
92 }
98 // === Constants ===
99
100 /// Global ownership table for cache-aware resident file sub-agents (#529).
101 /// Maps file path → agent id. Agents hold a lease on a file while running;
102 /// the lease is released when the agent reaches a terminal state.
103 static RESIDENT_LEASES: std::sync::OnceLock<
104 parking_lot::Mutex<std::collections::HashMap<String, String>>,
105 > = std::sync::OnceLock::new();
106 const MAX_RESIDENT_CONTEXT_BYTES: u64 = 64 * 1024;
107
108 /// Release all resident file leases held by `agent_id`. Called when an
109 /// agent transitions to a terminal state (completed, failed, cancelled).
110 fn release_resident_leases_for(agent_id: &str) {
111 if let Some(lock) = RESIDENT_LEASES.get() {
112 let mut guard = lock.lock();
113 guard.retain(|_, owner| owner != agent_id);
114 }
115 }
116
117 fn reserve_resident_lease(lease_key: &str, display_path: &str) -> Result<(), ToolError> {
118 let leases = RESIDENT_LEASES.get_or_init(|| parking_lot::Mutex::new(HashMap::new()));
119 let mut guard = leases.lock();
120 if let Some(owner) = guard.get(lease_key) {
121 return Err(ToolError::invalid_input(format!(
122 "resident_file '{display_path}' is already leased by agent {owner}"
… 95 lines omitted; exact range 98–228 …
218 normalize_claim_path(&relative.to_string_lossy()).map_err(ToolError::permission_denied)?;
219 Ok(ResidentContext {
220 display_path,
221 // The lease table is process-wide, so a repo-relative path alone
222 // would falsely collide across unrelated workspaces. The authority
223 // resolver returned a canonical in-workspace file; use that exact
224 // identity internally while keeping only the relative label visible.
225 lease_key: path.to_string_lossy().into_owned(),
226 contents,
227 })
228 }
1 //! Fleet worker host adapters.
2 //!
3 //! Adapters own process boundaries for worker hosts. The manager can lease and
4 //! observe work through this trait without knowing whether the worker is a
5 //! local child process or an SSH-backed remote command.
262 /// Persistent storage for conversation threads, messages, checkpoints, and jobs.
263 ///
264 /// Backed by a SQLite database and an append-only JSONL session index file.
265 /// The database schema is automatically initialized and migrated on [`open`](Self::open).
266 #[derive(Debug, Clone)]
267 pub struct StateStore {
268 db_path: PathBuf,
269 session_index_path: PathBuf,
270 // Single long-lived connection shared by all clones. SQLite pragmas are
271 // per-connection, so opening once in `open` and applying them there keeps
272 // every operation consistent without re-opening the database per call.
273 conn: Arc<Mutex<Connection>>,
274 }
275
276 impl StateStore {
277 /// Open (or create) a state store at the given database path.
278 ///
279 /// If `path` is `None`, the default location (`~/.codewhale/state.db`, with
280 /// `~/.deepseek/state.db` as a legacy fallback) is used.
281 /// The database schema is created automatically if it does not exist.
282 pub fn open(path: Option<PathBuf>) -> Result<Self> {
283 let db_path = path.unwrap_or_else(default_state_db_path);
284 let session_index_path = db_path
285 .parent()
286 .unwrap_or_else(|| Path::new("."))
… 41 lines omitted; exact range 262–338 …
328 let configured_mode: String = conn
329 .pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get(0))
330 .with_context(|| format!("failed to enable WAL for {}", db_path.display()))?;
331 if !configured_mode.eq_ignore_ascii_case("wal") {
332 anyhow::bail!(
333 "failed to enable WAL for {}: SQLite retained journal mode {configured_mode}",
334 db_path.display()
335 );
336 }
337 }
338 Ok(())
1 //! Workspace snapshots — pre/post-turn safety net.
2 //!
3 //! Each turn the engine takes a `pre-turn:<seq>` snapshot of the user's
4 //! workspace into a side git repo at
5 //! `~/.deepseek/snapshots/<project_hash>/<worktree_hash>/.git`, then a
6 //! matching `post-turn:<seq>` snapshot when the turn finishes. Users
7 //! can roll back via `/restore N` (slash command) or, when the model
8 //! recognises an "undo my last edit" intent, the `revert_turn` tool.
9 //!
10 //! ## Why a side repo?
11 //!
12 //! - The user's own `.git` is never touched. `--git-dir` and
13 //! `--work-tree` are *always* set together when we shell out to git;
14 //! that single invariant is what keeps snapshots and the user's repo
15 //! completely independent.
16 //! - Workspaces without git still get snapshots.
17 //! - `git`'s own deduplication (object packfiles) keeps the disk
18 //! footprint tractable — typical 100 MB workspace × 12 turns ≈ 1.2 GB
19 //! uncompressed but git's content-addressed storage usually brings
20 //! that down 10-30×. We mitigate further with:
21 //! - 7-day default retention (`session_manager` prunes at session
22 //! start via [`prune::prune_older_than`]).
23 //! - `gc.auto = 0` on the side repo (we don't want background gcs
24 //! firing mid-turn) plus an explicit `git gc --prune=now` after
25 //! prune.
26 //! - Startup cleanup for stale `tmp_pack_*` files left by interrupted
27 //! git pack operations.
28 //!
29 //! ## Failure model
30 //!
31 //! Pre/post-turn snapshot calls are **non-fatal**. If `git` is missing,
32 //! the disk is full, or the workspace is on a read-only filesystem, the
33 //! turn proceeds and the engine logs a warning. The snapshot is a
34 //! safety net, not a correctness gate.
19 /// Final status for a turn.
20 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21 pub enum TurnOutcomeStatus {
22 Completed,
23 Interrupted,
24 Failed,
25 }
26
27 /// Provider/model route resolved for a model-backed turn.
28 ///
29 /// Emitted at `RouteDispatched` so hosts retain provenance until the matching
30 /// `TurnComplete` without relying on mutable global selection state. Non-model
31 /// turns such as composer `!` shell commands use no route.
32 #[derive(Debug, Clone, PartialEq, Eq)]
33 pub struct TurnRoute {
34 pub provider: ApiProvider,
35 /// Exact non-secret configured route key. Named custom providers all map
36 /// to [`ApiProvider::Custom`], so the enum alone is not provenance.
37 pub provider_identity: String,
38 pub model: String,
39 pub auto_model: bool,
40 /// Secret-free proof of the endpoint and credential generation the turn's
41 /// client was *installed* on, minted from that client rather than re-read
42 /// from config later.
43 ///
… 51 lines omitted; exact range 19–105 …
95 /// - This envelope is stamped at the **wire** boundary and answers *what was
96 /// actually put on the wire, when*. A planned-but-unsent route has no
97 /// metering surface and no dispatch instant, so it must be structurally
98 /// absent rather than defaulted.
99 #[derive(Debug, Clone, PartialEq, Eq)]
100 pub struct RouteBillingEnvelope {
101 pub billing_surface: Option<String>,
102 pub endpoint_fingerprint: Option<String>,
103 pub billing_mode: crate::cost_status::RouteBillingMode,
104 pub dispatched_at: DateTime<Utc>,
105 }