ci: add GitHub Actions workflows with semantic-release auto-versioning
chore: fix all 702 clippy warnings across codebase - auto-fix 475 via cargo clippy --fix - fix remaining 227 manually: uninlined_format_args, redundant_closure, match_same_arms, underscore_binding, format_push_string, items_after_statements, needless_pass_by_value, clone_on_copy, case_sensitive_extension, single_match/let-else, write_with_newline, and other clippy lints
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
//! loop calls `apply_action(&mut state, action)` → for turn-producing
|
||||
//! actions (`SubmitInput`), `spawn_turn` is kicked off on a background OS
|
||||
//! thread which drives `run_agent_turn` (stream to the LLM, gate and
|
||||
//! execute tool calls via `Harness`, archive messages to SQLite, log edits)
|
||||
//! execute tool calls via `Harness`, archive messages to `SQLite`, log edits)
|
||||
//! and pushes `TurnEvent`s onto a shared queue → on the next `Tick`, queued
|
||||
//! `TurnEvent`s are drained back into `AppStateRest` (transcript, toasts,
|
||||
//! usage counters).
|
||||
@@ -16,7 +16,10 @@
|
||||
//! running turns on plain OS threads (rather than blocking the main loop)
|
||||
//! keeps the TUI responsive while the LLM streams.
|
||||
|
||||
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::app::harness::Verdict;
|
||||
use sha2::Digest;
|
||||
@@ -30,7 +33,7 @@ use crate::dto::chat::message::{ChatMessage, Role};
|
||||
/// streaming pipeline, or subagent threads — that mutates `AppStateRest`
|
||||
/// when applied via `apply_action`.
|
||||
///
|
||||
/// Step bounds intentionally left unbounded (usize::MAX) so the agent can
|
||||
/// Step bounds intentionally left unbounded (`usize::MAX`) so the agent can
|
||||
/// continue across as many turns as needed. Each iteration still honours
|
||||
/// `tc.abort_flag` and the per-call LLM timeout, so a runaway loop is
|
||||
/// observable and cancellable from the UI.
|
||||
@@ -99,6 +102,7 @@ pub enum Action {
|
||||
/// need to know how to *produce* actions.
|
||||
///
|
||||
/// Return: nothing; `state` is mutated in place.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
match action {
|
||||
Action::ForceQuit => {
|
||||
@@ -168,37 +172,36 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
Ok(abs_path) => {
|
||||
let content = std::fs::read_to_string(&abs_path)
|
||||
.unwrap_or_default();
|
||||
let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
|
||||
let lines: Vec<String> = content.lines().map(std::string::ToString::to_string).collect();
|
||||
let ed = crate::app::mode::editor::EditorState::open(
|
||||
abs_path.to_string_lossy().to_string(),
|
||||
Some(lines),
|
||||
);
|
||||
state.misc.editor = Some(ed);
|
||||
state.misc.overlay = Overlay::Editor;
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("Editing {}", path)));
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("Editing {path}")));
|
||||
}
|
||||
Err(e) => {
|
||||
state.push_toast(Toast::new(ToastKind::Error, format!("Failed to open {}: {}", path, e)));
|
||||
state.push_toast(Toast::new(ToastKind::Error, format!("Failed to open {path}: {e}")));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::McpAdd { name, command } => {
|
||||
let extra_args: Vec<String> = command.split_whitespace().map(|s| s.to_string()).collect();
|
||||
let extra_args: Vec<String> = command.split_whitespace().map(std::string::ToString::to_string).collect();
|
||||
let cmd = extra_args.first().cloned().unwrap_or_default();
|
||||
let args: Vec<String> = extra_args.into_iter().skip(1).collect();
|
||||
match state.mcp_manager.connect_stdio(&name, &cmd, &args) {
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
let tool_count = state.mcp_manager.servers.last()
|
||||
.map(|s| s.tools.len())
|
||||
.unwrap_or(0);
|
||||
.map_or(0, |s| s.tools.len());
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("Connected MCP server '{}' ({} tools)", name, tool_count)));
|
||||
format!("Connected MCP server '{name}' ({tool_count} tools)")));
|
||||
state.dirty = true;
|
||||
}
|
||||
Err(e) => {
|
||||
state.push_toast(Toast::new(ToastKind::Error,
|
||||
format!("MCP connect failed: {}", e)));
|
||||
format!("MCP connect failed: {e}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +241,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
let result = run_oauth_flow(&provider_clone);
|
||||
let message = match result {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => format!("OAuth login failed: {}", e),
|
||||
Err(e) => format!("OAuth login failed: {e}"),
|
||||
};
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
@@ -247,7 +250,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
});
|
||||
}
|
||||
});
|
||||
let toast = Toast::new(ToastKind::Info, format!("Opening browser for {} login...", provider));
|
||||
let toast = Toast::new(ToastKind::Info, format!("Opening browser for {provider} login..."));
|
||||
state.push_toast(toast);
|
||||
state.dirty = true;
|
||||
}
|
||||
@@ -325,13 +328,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
let display_path = path.unwrap_or_default();
|
||||
let display = if tool_name == "read" {
|
||||
let line_count = output.lines().count();
|
||||
if !display_path.is_empty() {
|
||||
format!("read: {} ({} lines)", display_path, line_count)
|
||||
if display_path.is_empty() {
|
||||
format!("read: {line_count} line(s)")
|
||||
} else {
|
||||
format!("read: {} line(s)", line_count)
|
||||
format!("read: {display_path} ({line_count} lines)")
|
||||
}
|
||||
} else {
|
||||
format!("{}: {}", tool_name, output)
|
||||
format!("{tool_name}: {output}")
|
||||
};
|
||||
state.push_transcript(ChatMessageDisplay::new(
|
||||
Role::Tool,
|
||||
@@ -356,7 +359,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
}
|
||||
if should_trigger_review(state, Origin::Main) {
|
||||
let _ = trigger_review(state);
|
||||
trigger_review(state);
|
||||
}
|
||||
} else if kind == "review" {
|
||||
let counted = if let Some(ref mut rt) = state.session_runtime {
|
||||
@@ -422,7 +425,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
});
|
||||
state.push_transcript(ChatMessageDisplay::new(
|
||||
crate::dto::chat::message::Role::System,
|
||||
format!("✓ {}", message),
|
||||
format!("✓ {message}"),
|
||||
));
|
||||
if state.misc.overlay == Overlay::Workflow {
|
||||
state.misc.overlay = Overlay::None;
|
||||
@@ -437,7 +440,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
});
|
||||
state.push_transcript(ChatMessageDisplay::new(
|
||||
crate::dto::chat::message::Role::System,
|
||||
format!("✗ {}", message),
|
||||
format!("✗ {message}"),
|
||||
));
|
||||
if state.misc.overlay == Overlay::Workflow {
|
||||
state.misc.overlay = Overlay::None;
|
||||
@@ -486,7 +489,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.push_toast(long_toast);
|
||||
state.push_transcript(ChatMessageDisplay::new(
|
||||
crate::dto::chat::message::Role::System,
|
||||
format!("Error: {}", msg),
|
||||
format!("Error: {msg}"),
|
||||
));
|
||||
turn_finished = true;
|
||||
}
|
||||
@@ -548,7 +551,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
let total_chars: usize = rt.messages.iter()
|
||||
.filter_map(|m| m.content.as_deref())
|
||||
.map(|c| c.len())
|
||||
.map(str::len)
|
||||
.sum();
|
||||
let token_estimate = total_chars / 3;
|
||||
rt.messages = crate::app::runtime::shortsend::shape_messages(&rt.messages, token_estimate, max_wire_tokens, true, None);
|
||||
@@ -566,7 +569,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("accepted lesson: {}", name)));
|
||||
format!("accepted lesson: {name}")));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonReject { name } => {
|
||||
@@ -579,7 +582,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info,
|
||||
format!("rejected lesson: {}", name)));
|
||||
format!("rejected lesson: {name}")));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonDelete { name } => {
|
||||
@@ -587,7 +590,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {}", name)));
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {name}")));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::RunPipeline { mode } => {
|
||||
@@ -606,10 +609,10 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
"status" => {
|
||||
let current = state.misc.pipeline_override.as_deref().unwrap_or("auto");
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {} (use /pipeline full|quick|skip to change)", current)));
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {current} (use /pipeline full|quick|skip to change)")));
|
||||
}
|
||||
_ => {
|
||||
state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {} (use: full, quick, skip)", mode)));
|
||||
state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {mode} (use: full, quick, skip)")));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
@@ -644,8 +647,8 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
// "prompt1 | prompt2 | prompt3" → Parallel of 3 agents
|
||||
// "prompt1 -> prompt2" → Pipeline of 2 stages
|
||||
// "prompt" → single Agent
|
||||
let parts_pipe: Vec<&str> = script.split('|').map(|s| s.trim()).collect();
|
||||
let parts_arrow: Vec<&str> = script.split("->").map(|s| s.trim()).collect();
|
||||
let parts_pipe: Vec<&str> = script.split('|').map(str::trim).collect();
|
||||
let parts_arrow: Vec<&str> = script.split("->").map(str::trim).collect();
|
||||
|
||||
let primitive = if parts_pipe.len() > 1 {
|
||||
ScriptPrimitive::Parallel(
|
||||
@@ -680,12 +683,12 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
|
||||
let args: HashMap<String, String> = HashMap::new();
|
||||
let result = crate::app::workflow::engine::run_workflow_tracked(
|
||||
&wf, &args, Some(live), &session_dir, &workspace_roots,
|
||||
&wf, &args, Some(&live), &session_dir, &workspace_roots,
|
||||
);
|
||||
|
||||
let (kind, message) = match result {
|
||||
Ok(summary) => ("workflow_done".to_string(), summary),
|
||||
Err(e) => ("workflow_error".to_string(), format!("Workflow failed: {}", e)),
|
||||
Err(e) => ("workflow_error".to_string(), format!("Workflow failed: {e}")),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
@@ -793,7 +796,7 @@ fn spawn_turn(state: &AppStateRest) {
|
||||
abort_flag,
|
||||
pipeline_mode,
|
||||
};
|
||||
let result = run_agent_turn(tc, &messages, &events_q);
|
||||
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()));
|
||||
@@ -837,7 +840,7 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("Current Workspace Directory Structure:\n");
|
||||
for root in roots {
|
||||
out.push_str(&format!("Root: {}\n", root.display()));
|
||||
writeln!(out, "Root: {}", root.display()).unwrap();
|
||||
let walker = ignore::WalkBuilder::new(root)
|
||||
.hidden(true)
|
||||
.git_ignore(true)
|
||||
@@ -847,9 +850,9 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
|
||||
let path = entry.path();
|
||||
if let Ok(rel) = path.strip_prefix(root) {
|
||||
if rel.as_os_str().is_empty() { continue; }
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
|
||||
let prefix = if is_dir { "[DIR] " } else { " " };
|
||||
out.push_str(&format!(" {}{}\n", prefix, rel.display()));
|
||||
writeln!(out, " {}{}", prefix, rel.display()).unwrap();
|
||||
count += 1;
|
||||
if count > 1000 {
|
||||
out.push_str(" ... (truncated)\n");
|
||||
@@ -881,7 +884,7 @@ fn build_memory_section(memory_dir: &std::path::Path) -> String {
|
||||
}
|
||||
|
||||
let mut section = String::from("\n\n--- Persistent Memory ---\n");
|
||||
section.push_str(&format!("Total entries: {}\n\n", names.len()));
|
||||
write!(section, "Total entries: {}\n\n", names.len()).unwrap();
|
||||
|
||||
for name in &names {
|
||||
if section.len() > 3000 {
|
||||
@@ -892,7 +895,7 @@ fn build_memory_section(memory_dir: &std::path::Path) -> String {
|
||||
if mem.lifecycle == "stale" {
|
||||
continue;
|
||||
}
|
||||
section.push_str(&format!("## [{}] {}\n{}\n\n", mem.kind, mem.name, mem.content));
|
||||
write!(section, "## [{}] {}\n{}\n\n", mem.kind, mem.name, mem.content).unwrap();
|
||||
}
|
||||
}
|
||||
section.push_str("---");
|
||||
@@ -940,13 +943,13 @@ fn refresh_lesson_counters(memory_dir: &std::path::Path, rt: &mut crate::app::st
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist a `ChatMessage` to the SQLite message log, if a database
|
||||
/// Persist a `ChatMessage` to the `SQLite` message log, if a database
|
||||
/// connection is available.
|
||||
///
|
||||
/// Flow: if `db` is `Some`, lock the mutex and call `insert_message`.
|
||||
/// Errors are silently ignored.
|
||||
fn archive_message(db: &Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>, session_id: &str, msg: &ChatMessage) {
|
||||
if let Some(ref arc) = db {
|
||||
fn archive_message(db: Option<&std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>, session_id: &str, msg: &ChatMessage) {
|
||||
if let Some(arc) = db {
|
||||
if let Ok(conn) = arc.lock() {
|
||||
let _ = crate::model::msglog::insert_message(&conn, session_id, msg);
|
||||
}
|
||||
@@ -979,7 +982,7 @@ const MAX_AUTO_REVIEWS_PER_TURN: usize = 2;
|
||||
/// through `Harness::gate_tool_call`) or unwrap the final assistant
|
||||
/// message → check for unfinished todo.md tasks (auto-retry with a
|
||||
/// system message if any remain) → finalise with `Done` and an `edits`
|
||||
/// SystemNote.
|
||||
/// `SystemNote`.
|
||||
///
|
||||
/// On streaming failure: retry once with a non-streaming call → if that
|
||||
/// also fails and there are unfinished tasks, sleep 5s and loop back;
|
||||
@@ -990,11 +993,13 @@ const MAX_AUTO_REVIEWS_PER_TURN: usize = 2;
|
||||
///
|
||||
/// Return: `Ok(())` on successful completion, or an error from the LLM
|
||||
/// API after retries are exhausted.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_agent_turn(
|
||||
tc: TurnCtx,
|
||||
tc: &TurnCtx,
|
||||
messages: &[ChatMessage],
|
||||
events_q: &std::sync::Arc<std::sync::Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
const MAX_TODO_RETRIES: usize = 5;
|
||||
let mut msgs = messages.to_vec();
|
||||
let mut edits_this_turn = 0u32;
|
||||
let mut edited_paths: Vec<String> = Vec::new();
|
||||
@@ -1016,7 +1021,7 @@ fn run_agent_turn(
|
||||
);
|
||||
if !msgs.iter().any(|m| matches!(m.role, crate::dto::chat::message::Role::System)) {
|
||||
let sys = ChatMessage::system(system_text);
|
||||
archive_message(&tc.db, &tc.session_id, &sys);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &sys);
|
||||
msgs.insert(0, sys);
|
||||
}
|
||||
|
||||
@@ -1032,24 +1037,21 @@ fn run_agent_turn(
|
||||
.count();
|
||||
let should_pipeline = if user_msg_count <= 2 {
|
||||
let user_request = msgs.iter()
|
||||
.rev()
|
||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.next()
|
||||
.rev().find(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.and_then(|m| m.content.as_deref())
|
||||
.unwrap_or("");
|
||||
|
||||
if !user_request.is_empty() {
|
||||
if user_request.is_empty() {
|
||||
false
|
||||
} else {
|
||||
match tc.pipeline_mode.as_deref() {
|
||||
Some("skip") => {
|
||||
tracing::debug!("[ceo] pipeline skipped via /pipeline skip");
|
||||
false
|
||||
}
|
||||
Some("full") => true,
|
||||
Some("quick") => true,
|
||||
Some("full" | "quick") => true,
|
||||
_ => crate::app::workflow::company::is_complex_request(user_request),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
@@ -1057,9 +1059,7 @@ fn run_agent_turn(
|
||||
|
||||
if should_pipeline {
|
||||
let user_request = msgs.iter()
|
||||
.rev()
|
||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.next()
|
||||
.rev().find(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.and_then(|m| m.content.as_deref())
|
||||
.unwrap_or("");
|
||||
|
||||
@@ -1102,26 +1102,23 @@ fn run_agent_turn(
|
||||
Ok(summary) => {
|
||||
tracing::info!("[ceo] company pipeline completed successfully");
|
||||
let pipeline_msg = ChatMessage::system(format!(
|
||||
"[Company Pipeline: {}]\n{}",
|
||||
mode_label,
|
||||
summary,
|
||||
"[Company Pipeline: {mode_label}]\n{summary}",
|
||||
));
|
||||
archive_message(&tc.db, &tc.session_id, &pipeline_msg);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &pipeline_msg);
|
||||
msgs.push(pipeline_msg);
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: format!("Company pipeline ({}) complete. CEO reviewing results...", mode_label),
|
||||
message: format!("Company pipeline ({mode_label}) complete. CEO reviewing results..."),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[ceo] company pipeline failed: {}", e);
|
||||
let fail_msg = ChatMessage::system(format!(
|
||||
"[Pipeline Note] The company pipeline encountered issues: {}.\n\
|
||||
"[Pipeline Note] The company pipeline encountered issues: {e}.\n\
|
||||
Proceeding with direct execution as fallback.",
|
||||
e,
|
||||
));
|
||||
msgs.push(fail_msg);
|
||||
}
|
||||
@@ -1132,15 +1129,13 @@ fn run_agent_turn(
|
||||
|
||||
let mut turn_step = 0usize;
|
||||
let mut todo_retry_count = 0usize;
|
||||
const MAX_TODO_RETRIES: usize = 5;
|
||||
|
||||
loop {
|
||||
turn_step += 1;
|
||||
if turn_step > MAX_TURN_STEPS {
|
||||
anyhow::bail!(
|
||||
"turn exceeded maximum steps ({}) — possible runaway loop. \
|
||||
"turn exceeded maximum steps ({MAX_TURN_STEPS}) — possible runaway loop. \
|
||||
aborting to prevent excessive token usage",
|
||||
MAX_TURN_STEPS,
|
||||
);
|
||||
}
|
||||
if turn_start_ms.elapsed().as_millis() as u64 > MAX_TURN_TIMEOUT_MS {
|
||||
@@ -1152,7 +1147,7 @@ fn run_agent_turn(
|
||||
}
|
||||
let total_chars: usize = msgs.iter()
|
||||
.filter_map(|m| m.content.as_deref())
|
||||
.map(|c| c.len())
|
||||
.map(str::len)
|
||||
.sum();
|
||||
let token_estimate = total_chars / 4;
|
||||
let max_wire_tokens = tc.context_window;
|
||||
@@ -1168,7 +1163,7 @@ fn run_agent_turn(
|
||||
}
|
||||
|
||||
// Also update our local `msgs` variable so the rest of the loop operates on the compacted version
|
||||
msgs = compacted.clone();
|
||||
msgs.clone_from(&compacted);
|
||||
compacted
|
||||
} else {
|
||||
prev_shaped = false;
|
||||
@@ -1251,15 +1246,14 @@ fn run_agent_turn(
|
||||
todo_retry_count += 1;
|
||||
if todo_retry_count > MAX_TODO_RETRIES {
|
||||
anyhow::bail!(
|
||||
"exhausted {} todo-retries — giving up on unfinished tasks. \
|
||||
"exhausted {MAX_TODO_RETRIES} todo-retries — giving up on unfinished tasks. \
|
||||
Edit todo.md manually or ask me to focus on specific items.",
|
||||
MAX_TODO_RETRIES,
|
||||
);
|
||||
}
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!("Network/API error: {}. Auto-retrying to finish tasks... (retry {}/{})", api_err, todo_retry_count, MAX_TODO_RETRIES),
|
||||
message: format!("Network/API error: {api_err}. Auto-retrying to finish tasks... (retry {todo_retry_count}/{MAX_TODO_RETRIES})"),
|
||||
});
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
@@ -1283,7 +1277,7 @@ fn run_agent_turn(
|
||||
let content = response.content.clone().unwrap_or_default();
|
||||
if has_tool_calls {
|
||||
let tool_calls = response.tool_calls.clone().unwrap_or_default();
|
||||
archive_message(&tc.db, &tc.session_id, &response);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &response);
|
||||
msgs.push(response);
|
||||
for tool_call in tool_calls {
|
||||
if tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
@@ -1298,7 +1292,7 @@ fn run_agent_turn(
|
||||
);
|
||||
|
||||
let ws_roots: Vec<&std::path::Path> =
|
||||
tc.workspace_roots.iter().map(|p| p.as_path()).collect();
|
||||
tc.workspace_roots.iter().map(std::path::PathBuf::as_path).collect();
|
||||
let verdict = crate::app::harness::Harness::gate_tool_call(
|
||||
&tool_name,
|
||||
&args,
|
||||
@@ -1316,12 +1310,12 @@ fn run_agent_turn(
|
||||
&args,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.session_id,
|
||||
&tc.db,
|
||||
tc.db.as_ref(),
|
||||
) {
|
||||
Ok(result) => (result, false, is_edit_tool),
|
||||
Err(e) => (e.to_string(), true, false),
|
||||
},
|
||||
Verdict::Block(reason) => (format!("Blocked: {}", reason), true, false),
|
||||
Verdict::Block(reason) => (format!("Blocked: {reason}"), true, false),
|
||||
};
|
||||
|
||||
if is_edit {
|
||||
@@ -1332,7 +1326,7 @@ fn run_agent_turn(
|
||||
// background subagent tracking.
|
||||
let edit_path = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.map(std::string::ToString::to_string);
|
||||
if let Some(ref p) = edit_path {
|
||||
edited_paths.push(p.clone());
|
||||
|
||||
@@ -1353,7 +1347,7 @@ fn run_agent_turn(
|
||||
Ok(verdict) => {
|
||||
let elapsed = review_start.elapsed().as_millis();
|
||||
let review_msg = ChatMessage::tool_result(
|
||||
format!("auto-review-{}", inline_reviews_count),
|
||||
format!("auto-review-{inline_reviews_count}"),
|
||||
format!(
|
||||
"[Auto inline review: {} ({}ms)]\n{}",
|
||||
p,
|
||||
@@ -1361,7 +1355,7 @@ fn run_agent_turn(
|
||||
verdict.trim(),
|
||||
),
|
||||
);
|
||||
archive_message(&tc.db, &tc.session_id, &review_msg);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &review_msg);
|
||||
msgs.push(review_msg);
|
||||
tracing::info!(
|
||||
"[auto-review] inline review for '{}' completed in {}ms: {}",
|
||||
@@ -1381,7 +1375,7 @@ fn run_agent_turn(
|
||||
}
|
||||
|
||||
|
||||
let tool_path = args.get("path").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let tool_path = args.get("path").and_then(|v| v.as_str()).map(std::string::ToString::to_string);
|
||||
|
||||
{
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
@@ -1396,12 +1390,12 @@ fn run_agent_turn(
|
||||
}
|
||||
|
||||
let tool_msg = ChatMessage::tool_result(tool_call.id.clone(), output);
|
||||
archive_message(&tc.db, &tc.session_id, &tool_msg);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &tool_msg);
|
||||
msgs.push(tool_msg);
|
||||
}
|
||||
} else {
|
||||
if !content.is_empty() {
|
||||
archive_message(&tc.db, &tc.session_id, &response);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &response);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
if stream_started {
|
||||
q.push_back(TurnEvent::StreamDone(response.clone()));
|
||||
@@ -1425,15 +1419,15 @@ fn run_agent_turn(
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!("Giving up after {} retries — some todo items remain unfinished. Edit todo.md manually or ask again.", MAX_TODO_RETRIES),
|
||||
message: format!("Giving up after {MAX_TODO_RETRIES} retries — some todo items remain unfinished. Edit todo.md manually or ask again."),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
let sys_text = format!("You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished. (Retry {}/{})", todo_retry_count, MAX_TODO_RETRIES);
|
||||
let sys_text = format!("You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished. (Retry {todo_retry_count}/{MAX_TODO_RETRIES})");
|
||||
let sys_text_clone = sys_text.clone();
|
||||
let msg = ChatMessage::system(sys_text);
|
||||
archive_message(&tc.db, &tc.session_id, &msg);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &msg);
|
||||
msgs.push(msg);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
@@ -1510,13 +1504,13 @@ fn execute_one_tool(
|
||||
args: &serde_json::Value,
|
||||
session_dir: &std::path::Path,
|
||||
session_id: &str,
|
||||
db: &Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
|
||||
db: Option<&std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
|
||||
) -> anyhow::Result<String> {
|
||||
for tool in tools {
|
||||
if tool.name() == name {
|
||||
// Snapshot current file content before write/edit for rewind
|
||||
if (name == "write" || name == "edit") && !tool_call_id.is_empty() {
|
||||
if let Some(ref arc) = db {
|
||||
if let Some(arc) = db {
|
||||
if let Ok(conn) = arc.lock() {
|
||||
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if let Ok(abs_path) = crate::tool::resolve_path(&ctx.workspaces, path) {
|
||||
@@ -1544,13 +1538,12 @@ fn execute_one_tool(
|
||||
let hash = sha2::Sha256::digest(
|
||||
content.and_then(|v| v.as_str()).unwrap_or("").as_bytes(),
|
||||
);
|
||||
format!("{:x}", hash)
|
||||
format!("{hash:x}")
|
||||
};
|
||||
let bytes_delta = if name == "write" {
|
||||
args.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.len() as i64)
|
||||
.unwrap_or(0)
|
||||
.map_or(0, |s| s.len() as i64)
|
||||
} else {
|
||||
let old = args.get("old").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let new = args.get("new").and_then(|v| v.as_str()).unwrap_or("");
|
||||
@@ -1572,7 +1565,7 @@ fn execute_one_tool(
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
anyhow::bail!("tool not found: {}", name)
|
||||
anyhow::bail!("tool not found: {name}")
|
||||
}
|
||||
|
||||
/// Optionally push a review-available toast at the end of a turn that
|
||||
@@ -1591,14 +1584,13 @@ fn maybe_trigger_review(state: &mut AppStateRest) {
|
||||
let edit_count = state
|
||||
.session_runtime
|
||||
.as_ref()
|
||||
.map(|rt| rt.edit_count)
|
||||
.unwrap_or(0);
|
||||
.map_or(0, |rt| rt.edit_count);
|
||||
if edit_count == 0 {
|
||||
return;
|
||||
}
|
||||
state.push_toast(Toast::new(
|
||||
ToastKind::Info,
|
||||
format!("{} file(s) modified this session. Review available.", edit_count),
|
||||
format!("{edit_count} file(s) modified this session. Review available."),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1698,13 +1690,13 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||
let code = server.wait_for_code(120_000, &state_token)?;
|
||||
|
||||
manager.exchange_code(&code, &redirect_uri, verifier.as_str())
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
if let Some(ref token) = manager.token {
|
||||
let token_path = dirs::config_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("zesdex")
|
||||
.join(format!("oauth_{}.json", provider));
|
||||
.join(format!("oauth_{provider}.json"));
|
||||
if let Some(parent) = token_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
@@ -1713,7 +1705,7 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!("Successfully authenticated with {}.", provider))
|
||||
Ok(format!("Successfully authenticated with {provider}."))
|
||||
}
|
||||
|
||||
/// Spawn a background thread that checks API reachability via a lightweight HEAD
|
||||
@@ -1722,16 +1714,14 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||
///
|
||||
/// Flow: resolve the provider's base URL → build a short-lived reqwest client
|
||||
/// with 3s connect / 5s total timeout → HEAD the `/models` endpoint → push
|
||||
/// a `connectivity` SystemNote with the result.
|
||||
/// a `connectivity` `SystemNote` with the result.
|
||||
///
|
||||
/// Why: runs off the event loop so a slow/TIMEOUT network does not block the TUI.
|
||||
fn spawn_api_connectivity_check(state: &AppStateRest) {
|
||||
let base_url = state
|
||||
.app_config
|
||||
.providers
|
||||
.get(&state.settings.provider)
|
||||
.map(|p| p.api_base.clone())
|
||||
.unwrap_or_else(|| crate::service::provider::DEFAULT_BASE_URL.to_string());
|
||||
.get(&state.settings.provider).map_or_else(|| crate::service::provider::DEFAULT_BASE_URL.to_string(), |p| p.api_base.clone());
|
||||
let turn_events = state.turn_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
|
||||
Reference in New Issue
Block a user