refactor: massive codebase restructuring — naming, splitting, DRY

Crate renames:
  - zesdex-entities::seaorm → domain (misleading name, no SeaORM used)
  - zesdex-dto → merged into zesdex-entities (100% re-exports)
  - zesdex-libs → zesdex-infra (vague name)

Module renames:
  - app/harness → guard (misleading: safety gatekeeper, not test harness)
  - runtime/commands → action_dispatch (name clashed with controller/command)
  - resources → prompts (embedded prompt text, not general resources)
  - tool/seqthink → sequential_think (unreadable abbreviation)
  - msglog/query → insert (module only inserts, never queries)

Dead code removal:
  - app/mode/help.rs (orphaned — not declared in mod.rs)
  - app/mode/loading.rs (orphaned — not declared in mod.rs)

File splitting (71 new files, avg ~115 lines/file):
  - app/runtime/actions/: 1→8 files (was 2030 lines)
  - view/overlays/: 1→16 files (was 1167 lines)
  - tool/lsp/: 1→8 per-tool files (was 909 lines)
  - main.rs: 1→5 files (session, daemon, attach, event_loop)
  - workflow/engine + hive_mind: 2→10 files
  - subagent/engine + auto: 2→9 files
  - lsp/provisioner: 1→5 files
  - review/: 1→6 files
  - guard/: 1→2 files (extracted patterns)
  - state/misc: 1→3 files (input, scroll)
  - mcp/: 1→3 files (transport, adapter)
  - stream/json_repair extracted from turn.rs

DRY:
  - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent
  - 3 near-identical background spawners → 1 generic + thin wrappers
  - Shared spawn_subagent_with_drain() extracted
  - Shared create_session() in main
  - write_osc52 deduplicated

Bug fixes:
  - archive_message(): sess.db → db (wrong variable name)
  - execute_one_tool(): wrong parameter name
  - check_credential_read() function was missing (restored from test expectations)
This commit is contained in:
asepharyana
2026-07-17 09:08:41 +07:00
parent 1f0ae9f551
commit 9a67137954
139 changed files with 9704 additions and 8858 deletions
@@ -0,0 +1,169 @@
//! Turn-spawning logic: `spawn_turn` and the `TurnCtx` bundle passed to
//! the background thread that runs `run_agent_turn`.
use crate::app::state::rest::AppStateRest;
use crate::app::state::runtime::TurnEvent;
use super::turn::run_agent_turn;
/// Context bundle passed to `run_agent_turn` on its background thread.
pub(super) struct TurnCtx {
pub(super) client: crate::service::provider::LlmClient,
pub(super) tdefs: Vec<crate::dto::provider::request::ToolDef>,
pub(super) tools: Vec<Box<dyn crate::tool::Tool>>,
pub(super) ctx: crate::tool::ToolCtx,
pub(super) context_window: usize,
pub(super) workspace_roots: Vec<std::path::PathBuf>,
pub(super) edit_log_session_dir: std::path::PathBuf,
pub(super) session_id: String,
pub(super) db: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
pub(super) temperature: f32,
pub(super) max_tokens: Option<u32>,
pub(super) abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Snapshot of `SessionRuntime.hive_mind_converged` taken at the start
/// of this turn — whether a hive-mind convergence already completed
/// earlier in this session.
pub(super) hive_mind_converged: bool,
}
/// Spawn a background thread that runs one full LLM turn.
///
/// Flow: check that no turn is currently in-flight → bail if so →
/// collect messages and config from state → determine API key (from
/// settings, env var, or default) → resolve generation params from
/// the current effort level → collect all tools (built-in + MCP) →
/// build `TurnCtx` → spawn a thread running `run_agent_turn` →
/// on any error, push a `TurnEvent::Error` → clear the in-flight flag
/// when the thread exits.
///
/// Why: runs on a plain OS thread so the async event loop stays responsive.
///
/// Return: nothing; results flow through `state.turn_events`.
pub(super) fn spawn_turn(state: &AppStateRest) {
let in_flight = if let Ok(guard) = state.turn_in_flight.lock() {
*guard
} else {
return;
};
if in_flight {
return;
}
let messages = state
.session_runtime
.as_ref()
.map(|rt| rt.messages.clone())
.unwrap_or_default();
if messages.is_empty() {
return;
}
let mut api_key = state
.settings
.api_keys
.get(&state.settings.provider)
.cloned()
.unwrap_or_default();
let model = state.settings.model.clone();
let base_url = state
.app_config
.providers
.get(&state.settings.provider)
.map(|p| p.api_base.clone());
let context_window = state
.app_config
.model_roles
.values()
.find(|role| {
role.provider == state.settings.provider && role.model == state.settings.model
})
.and_then(|role| role.context_window)
.unwrap_or(state.app_config.default_context_window) as usize;
// The selected provider has no entry in app_config at all (e.g. the
// Claude-settings auto-detection that registers "claude" found nothing
// this run). Without this check, LlmClient::new silently falls back to
// the zen default base URL while keeping this provider's model name —
// a mismatched request that reaches a real server and comes back as a
// confusing "Missing API key" 401 from an unrelated provider, instead
// of the actual problem: the configured provider doesn't exist.
if base_url.is_none() {
if let Ok(mut q) = state.turn_events.lock() {
q.push_back(TurnEvent::Error(format!(
"Provider '{}' is not configured — no matching entry found. \
Pick a different provider in Settings, or configure it.",
state.settings.provider
)));
}
return;
}
if api_key.is_empty() {
if let Some(provider_cfg) = state.app_config.providers.get(&state.settings.provider) {
api_key = provider_cfg
.api_key_env
.as_ref()
.and_then(|env| std::env::var(env).ok())
.or_else(|| provider_cfg.default_api_key.clone())
.unwrap_or_default();
}
}
if api_key.is_empty() {
api_key = crate::service::provider::DEFAULT_API_KEY.to_string();
}
let (temperature, max_tokens) = crate::app::mode::effort::generation_params(
state.misc.effort_level,
state.settings.max_tokens,
);
let mut tools = crate::tool::all_tools();
tools.extend(state.mcp_manager.as_tools());
let tool_defs = crate::tool::tool_defs(&tools);
let ctx = state.tool_ctx();
let edit_session_dir = state.session_dir.clone();
let session_id = state.session_id.clone();
let turn_events = state.turn_events.clone();
let in_flight_flag = state.turn_in_flight.clone();
let workspace_roots: Vec<std::path::PathBuf> = ctx.workspaces.clone();
let abort_flag = state.abort_flag.clone();
abort_flag.store(false, std::sync::atomic::Ordering::SeqCst);
let hive_mind_converged = state
.session_runtime
.as_ref()
.is_some_and(|rt| rt.hive_mind_converged);
*in_flight_flag.lock().unwrap_or_else(|e| {
tracing::error!("[spawn_turn] in_flight_flag mutex poisoned: {}", e);
e.into_inner()
}) = true;
let events_q = turn_events.clone();
std::thread::spawn(move || {
let db = crate::model::msglog::open_or_create(&edit_session_dir)
.ok()
.map(|c| std::sync::Arc::new(std::sync::Mutex::new(c)));
let tc = TurnCtx {
client: crate::service::provider::LlmClient::new(api_key, model.clone(), base_url),
tdefs: tool_defs,
tools,
ctx,
context_window,
workspace_roots,
edit_log_session_dir: edit_session_dir,
session_id,
db,
temperature,
max_tokens,
abort_flag,
hive_mind_converged,
};
let result = run_agent_turn(&tc, &messages, &events_q);
if let Err(e) = result {
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::Error(e.to_string()));
}
}
if let Ok(mut flag) = in_flight_flag.lock() {
*flag = false;
}
});
}