feat: enhance prompts and validation for tool-call safety; enforce error handling and technical debt awareness
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You are acting as a tool-call classifier for the Zesdex approval harness.
|
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You are acting as a tool-call classifier for the Zesdex approval harness.
|
||||||
|
|
||||||
|
CRITICAL: Never ignore pre-existing errors, warnings, or technical debt. Fix them immediately. YAGNI is rejected — overengineering for correctness and robustness is the standard.
|
||||||
Given a tool name and its arguments, determine if the operation is safe.
|
Given a tool name and its arguments, determine if the operation is safe.
|
||||||
|
|
||||||
Classification rules:
|
Classification rules:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You are acting as a code quality reviewer for Zesdex. Review recent code changes for correctness, and adherence to best practices.
|
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You are acting as a code quality reviewer for Zesdex. Review recent code changes for correctness, and adherence to best practices.
|
||||||
|
|
||||||
|
CRITICAL: Never ignore pre-existing errors, warnings, or technical debt. Flag them for fixing immediately. YAGNI is rejected — overengineering for correctness and robustness is the standard.
|
||||||
|
|
||||||
You have read-only access to the workspace. Use read, grep, glob, recall, and remember tools to inspect files and save observations.
|
You have read-only access to the workspace. Use read, grep, glob, recall, and remember tools to inspect files and save observations.
|
||||||
|
|
||||||
Review guidelines:
|
Review guidelines:
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ You are Zesdex, an overengineering, perfectionist, and diligent programmer who d
|
|||||||
Core principles:
|
Core principles:
|
||||||
1. Be concise but thorough — prefer showing results over describing them.
|
1. Be concise but thorough — prefer showing results over describing them.
|
||||||
2. Deliver production-ready code — ensure absolutely zero placeholders, stubs, or lazy implementations (e.g., no `todo!()`, `pass`, or unfinished logic). Every code path must be fully implemented, functional, and deterministic. No dead code or redundant structures are allowed.
|
2. Deliver production-ready code — ensure absolutely zero placeholders, stubs, or lazy implementations (e.g., no `todo!()`, `pass`, or unfinished logic). Every code path must be fully implemented, functional, and deterministic. No dead code or redundant structures are allowed.
|
||||||
3. Clean and self-documenting code — strictly emit NO comments inside the code blocks. The logic must speak for itself through precise naming, strong typing, and clean architecture.
|
3. NEVER ignore pre-existing errors, warnings, or technical debt. If you encounter any existing issue (compiler warnings, lint errors, logic bugs, edge cases not handled), fix it immediately — do not leave it for later. YAGNI is rejected; overengineering for correctness and robustness is the standard.
|
||||||
|
4. Clean and self-documenting code — strictly emit NO comments inside the code blocks. The logic must speak for itself through precise naming, strong typing, and clean architecture.
|
||||||
4. Use the tools available to explore, understand, and modify the codebase.
|
4. Use the tools available to explore, understand, and modify the codebase.
|
||||||
5. For simple tasks, handle them directly with read/grep/write/edit.
|
5. For simple tasks, handle them directly with read/grep/write/edit.
|
||||||
6. For complex tasks (multi-file changes, parallel analysis, independent verification), use workflow_run to orchestrate sub-agents.
|
6. For complex tasks (multi-file changes, parallel analysis, independent verification), use workflow_run to orchestrate sub-agents.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You have access to the following tools. Use them to accomplish the user's request.
|
You are an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You have access to the following tools. Use them to accomplish the user's request.
|
||||||
|
|
||||||
|
CRITICAL: Never ignore pre-existing errors, warnings, or technical debt. Fix them immediately. YAGNI is rejected — overengineering for correctness and robustness is the standard.
|
||||||
For simple operations (read, grep, write small edits) use tools directly.
|
For simple operations (read, grep, write small edits) use tools directly.
|
||||||
For complex multi-step tasks that would benefit from parallel analysis or
|
For complex multi-step tasks that would benefit from parallel analysis or
|
||||||
independent verification, use workflow_run to orchestrate sub-agents.
|
independent verification, use workflow_run to orchestrate sub-agents.
|
||||||
|
|||||||
+294
-12
@@ -1,5 +1,7 @@
|
|||||||
//! Tool-call gating: decides whether a risky tool call is allowed to run
|
//! Tool-call gating: decides whether a risky tool call is allowed to run
|
||||||
//! before it executes.
|
//! before it executes. Implements hooks-style pre-checks for write/edit/delete
|
||||||
|
//! and bash tools so the agent cannot silently introduce stubs, denial
|
||||||
|
//! patterns, assumption language, or destructive commands.
|
||||||
|
|
||||||
/// Outcome of gating a tool call: whether it's allowed to run.
|
/// Outcome of gating a tool call: whether it's allowed to run.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -11,12 +13,111 @@ pub enum Verdict {
|
|||||||
/// Gatekeeper that decides whether a tool call may proceed before execution.
|
/// Gatekeeper that decides whether a tool call may proceed before execution.
|
||||||
pub struct Harness;
|
pub struct Harness;
|
||||||
|
|
||||||
|
/// Stub / placeholder / denial / assumption patterns that should never reach
|
||||||
|
/// a file in real code. Detected in write/edit content and bash heredocs.
|
||||||
|
const STUB_PATTERNS: &[&str] = &[
|
||||||
|
"todo!()",
|
||||||
|
"todo!(",
|
||||||
|
"unimplemented!()",
|
||||||
|
"unimplemented!(",
|
||||||
|
"todo_macro",
|
||||||
|
"FIXME",
|
||||||
|
"fixme:",
|
||||||
|
"XXX:",
|
||||||
|
"PLACEHOLDER",
|
||||||
|
"REPLACE_ME",
|
||||||
|
"stub_value",
|
||||||
|
"stub_function",
|
||||||
|
"fake_response",
|
||||||
|
"fake_data",
|
||||||
|
"not implemented",
|
||||||
|
"not yet implemented",
|
||||||
|
"to be implemented",
|
||||||
|
"to be done",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Language patterns indicating the AI is denying responsibility or
|
||||||
|
/// punting the work ("I'll skip this", "for now just", etc).
|
||||||
|
const DENIAL_PATTERNS: &[&str] = &[
|
||||||
|
"// skip",
|
||||||
|
"// skipping",
|
||||||
|
"// skipping for now",
|
||||||
|
"// for now just",
|
||||||
|
"// punt",
|
||||||
|
"// punted",
|
||||||
|
"// hack:",
|
||||||
|
"// hacky",
|
||||||
|
"// hack workaround",
|
||||||
|
"// workaround:",
|
||||||
|
"// cba",
|
||||||
|
"// later",
|
||||||
|
"// do later",
|
||||||
|
"// ignore for now",
|
||||||
|
"// disable",
|
||||||
|
"// disabled",
|
||||||
|
"// bypass",
|
||||||
|
"// quick fix",
|
||||||
|
"// temp fix",
|
||||||
|
"// temporary fix",
|
||||||
|
"// temp:",
|
||||||
|
"// temporary:",
|
||||||
|
"// noop",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Assumption-language patterns: words/phrases that indicate the code is
|
||||||
|
/// reasoning based on guesswork rather than data.
|
||||||
|
const ASSUMPTION_PATTERNS: &[&str] = &[
|
||||||
|
"// assume",
|
||||||
|
"// assuming",
|
||||||
|
"// probably",
|
||||||
|
"// maybe",
|
||||||
|
"// might",
|
||||||
|
"// should work",
|
||||||
|
"// hopefully",
|
||||||
|
"// guess",
|
||||||
|
"// i think",
|
||||||
|
"// should be fine",
|
||||||
|
"// should be",
|
||||||
|
"// likely",
|
||||||
|
"// ought to",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Network-exfiltration and credential-disclosure patterns for bash.
|
||||||
|
const EXFIL_PATTERNS: &[&str] = &[
|
||||||
|
"curl ", "wget ", "nc -e ", "ncat ", "/dev/tcp/",
|
||||||
|
"base64 -d |", "base64 --decode |",
|
||||||
|
"openssl s_client", "ssh -R ",
|
||||||
|
"scp /", "rsync /",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Substrings of well-known credential / secret files that bash must not read.
|
||||||
|
const SENSITIVE_PATH_PATTERNS: &[&str] = &[
|
||||||
|
".ssh/id_rsa",
|
||||||
|
".ssh/id_ed25519",
|
||||||
|
".ssh/authorized_keys",
|
||||||
|
".aws/credentials",
|
||||||
|
".aws/config",
|
||||||
|
".netrc",
|
||||||
|
".pypirc",
|
||||||
|
".npmrc",
|
||||||
|
".kube/config",
|
||||||
|
".docker/config.json",
|
||||||
|
".gnupg/",
|
||||||
|
"/etc/shadow",
|
||||||
|
"/etc/passwd",
|
||||||
|
"/proc/self/environ",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Minimum character length of a `reason` argument to be considered meaningful.
|
||||||
|
const MIN_REASON_LEN: usize = 8;
|
||||||
|
|
||||||
impl Harness {
|
impl Harness {
|
||||||
/// Decide whether a tool call is allowed to execute.
|
/// Decide whether a tool call is allowed to execute.
|
||||||
///
|
///
|
||||||
/// Flow: if the tool isn't flagged risky, allow immediately → basic
|
/// Flow: if the tool isn't flagged risky, allow immediately → file-tool
|
||||||
/// content checks (path traversal in paths AND command args) →
|
/// reason & path checks → content stub / denial / assumption scan →
|
||||||
/// workspace-root validation for output paths → classify.
|
/// bash destructive-pattern & exfiltration scan → workspace-root
|
||||||
|
/// validation for output paths.
|
||||||
///
|
///
|
||||||
/// Return: `Verdict::Allow` or `Verdict::Block(reason)`.
|
/// Return: `Verdict::Allow` or `Verdict::Block(reason)`.
|
||||||
pub fn gate_tool_call(
|
pub fn gate_tool_call(
|
||||||
@@ -28,31 +129,137 @@ impl Harness {
|
|||||||
if !crate::tool::tool_is_risky(tool_name) {
|
if !crate::tool::tool_is_risky(tool_name) {
|
||||||
return Verdict::Allow;
|
return Verdict::Allow;
|
||||||
}
|
}
|
||||||
// Path traversal check for file-mutating tools.
|
|
||||||
|
// File-mutating tools: write / edit / delete
|
||||||
if matches!(tool_name, "write" | "edit" | "delete") {
|
if matches!(tool_name, "write" | "edit" | "delete") {
|
||||||
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
|
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
|
||||||
if path.contains("..") {
|
if path.contains("..") {
|
||||||
return Verdict::Block("path traversal detected in 'path' argument".to_string());
|
return Verdict::Block(
|
||||||
|
"path traversal detected in 'path' argument".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !workspace_roots.is_empty() {
|
||||||
|
let abs_check = std::path::PathBuf::from(path);
|
||||||
|
if abs_check.is_absolute()
|
||||||
|
&& !workspace_roots.iter().any(|r| abs_check.starts_with(r))
|
||||||
|
{
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"absolute path '{path}' is outside all workspace roots"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Path traversal and dangerous content check for bash commands.
|
|
||||||
|
// write / edit require a non-trivial `reason` argument (hooks-style
|
||||||
|
// discipline: every mutation must explain itself).
|
||||||
|
if matches!(tool_name, "write" | "edit" | "delete") {
|
||||||
|
match Self::validate_reason(tool_name, args) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(msg) => return Verdict::Block(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write / edit content must not contain stubs, denial language, or
|
||||||
|
// assumption language.
|
||||||
|
if matches!(tool_name, "write" | "edit") {
|
||||||
|
if let Some(content) = Self::extract_content(tool_name, args) {
|
||||||
|
if let Some(pat) = Self::first_match(&content, STUB_PATTERNS) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"content contains stub/placeholder pattern '{pat}'; \
|
||||||
|
production code must be fully implemented — \
|
||||||
|
replace the stub with a real implementation"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(pat) = Self::first_match(&content, DENIAL_PATTERNS) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"content contains denial/punt pattern '{pat}'; \
|
||||||
|
implement the change properly instead of skipping"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(pat) = Self::first_match(&content, ASSUMPTION_PATTERNS) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"content contains assumption pattern '{pat}'; \
|
||||||
|
verify against data/tests instead of guessing"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bash: destructive patterns, exfiltration, sensitive-path reads.
|
||||||
if tool_name == "bash" {
|
if tool_name == "bash" {
|
||||||
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
if cmd.contains("..") {
|
if cmd.contains("..") {
|
||||||
return Verdict::Block("path traversal detected in bash command".to_string());
|
return Verdict::Block(
|
||||||
|
"path traversal detected in bash command".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !cmd.trim_start().starts_with("cargo")
|
||||||
|
&& !cmd.trim_start().starts_with("rustc")
|
||||||
|
&& !cmd.trim_start().starts_with("git ")
|
||||||
|
&& !cmd.trim_start().starts_with("ls")
|
||||||
|
&& !cmd.trim_start().starts_with("pwd")
|
||||||
|
&& !cmd.trim_start().starts_with("echo")
|
||||||
|
&& !cmd.trim_start().starts_with("cat")
|
||||||
|
&& !cmd.trim_start().starts_with("find")
|
||||||
|
&& !cmd.trim_start().starts_with("grep")
|
||||||
|
&& !cmd.trim_start().starts_with("test")
|
||||||
|
{
|
||||||
|
for pat in EXFIL_PATTERNS {
|
||||||
|
if cmd.contains(pat) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"potential data-exfiltration command blocked (matched '{pat}')"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for pat in SENSITIVE_PATH_PATTERNS {
|
||||||
|
if cmd.contains(pat) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"refused to read/write sensitive path '{pat}'"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let dangerous_patterns = [
|
let dangerous_patterns = [
|
||||||
"rm -rf /", "rm -rf --no-preserve-root",
|
"rm -rf /", "rm -rf --no-preserve-root",
|
||||||
"rm -rf ~", "rm -fr /", "mkfs.", "dd if=",
|
"rm -rf ~", "rm -fr /", "mkfs.", "dd if=",
|
||||||
":(){", "> /dev/sda", "chmod -R 000 /",
|
":(){", "> /dev/sda", "chmod -R 000 /",
|
||||||
|
"shutdown ", "poweroff ", "reboot ", "halt ",
|
||||||
];
|
];
|
||||||
for pat in &dangerous_patterns {
|
for pat in &dangerous_patterns {
|
||||||
if cmd.contains(pat) {
|
if cmd.contains(pat) {
|
||||||
return Verdict::Block(format!("destructive command pattern blocked: {}", pat));
|
return Verdict::Block(format!(
|
||||||
|
"destructive command pattern blocked: {pat}"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Also scan heredocs / -c / inline content for stub/denial
|
||||||
|
// language (e.g. `bash -c 'echo todo!()'`)
|
||||||
|
if let Some(pat) = Self::first_match(cmd, STUB_PATTERNS) {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"bash command contains stub pattern '{pat}'"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// git_operator: require a non-trivial reason as well.
|
||||||
|
if tool_name == "git_operator" {
|
||||||
|
if let Some(reason) = args.get("reason").and_then(|v| v.as_str()) {
|
||||||
|
if reason.trim().len() < MIN_REASON_LEN {
|
||||||
|
return Verdict::Block(format!(
|
||||||
|
"git_operator requires a non-trivial 'reason' \
|
||||||
|
(>= {MIN_REASON_LEN} chars) explaining the operation"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Verdict::Block(
|
||||||
|
"git_operator requires a 'reason' argument explaining the operation"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace-root validation for the resolved output path.
|
||||||
if let Some(out_path) = Self::find_output_path(tool_name, args) {
|
if let Some(out_path) = Self::find_output_path(tool_name, args) {
|
||||||
if !workspace_roots.is_empty()
|
if !workspace_roots.is_empty()
|
||||||
&& !out_path.starts_with("/tmp")
|
&& !out_path.starts_with("/tmp")
|
||||||
@@ -61,7 +268,8 @@ impl Harness {
|
|||||||
let allowed = workspace_roots.iter().any(|r| out_path.starts_with(r));
|
let allowed = workspace_roots.iter().any(|r| out_path.starts_with(r));
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return Verdict::Block(format!(
|
return Verdict::Block(format!(
|
||||||
"output path '{:?}' is outside all workspace roots", out_path
|
"output path '{:?}' is outside all workspace roots",
|
||||||
|
out_path
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,9 +277,83 @@ impl Harness {
|
|||||||
Self::classify(tool_name)
|
Self::classify(tool_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a candidate output path from a tool call, if one exists.
|
/// Validate the `reason` argument for a mutating tool.
|
||||||
///
|
///
|
||||||
/// Used to verify that writes and file mutations stay inside workspace roots.
|
/// Flow: require the field to exist and be a non-empty string ≥
|
||||||
|
/// `MIN_REASON_LEN` chars after trimming.
|
||||||
|
///
|
||||||
|
/// Why: hook-style gates force the agent to articulate the *why* of
|
||||||
|
/// every change, which both deters lazy writes and produces a useful
|
||||||
|
/// audit trail in the edit log.
|
||||||
|
fn validate_reason(tool_name: &str, args: &serde_json::Value) -> Result<(), String> {
|
||||||
|
let reason = match args.get("reason") {
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"{tool_name} requires a non-empty 'reason' argument \
|
||||||
|
explaining why the change is being made"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"{tool_name} 'reason' must be a string"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let trimmed = reason.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"{tool_name} 'reason' must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if trimmed.len() < MIN_REASON_LEN {
|
||||||
|
return Err(format!(
|
||||||
|
"{tool_name} 'reason' must be at least {MIN_REASON_LEN} chars \
|
||||||
|
(got {}) — explain WHY, not just WHAT",
|
||||||
|
trimmed.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Reject generic non-answers
|
||||||
|
let lower = trimmed.to_lowercase();
|
||||||
|
let non_answers = [
|
||||||
|
"fix", "update", "change", "edit", "modify",
|
||||||
|
"implement", "add", "remove", "delete",
|
||||||
|
"make it work", "make work", "test", "wip", "tbd",
|
||||||
|
];
|
||||||
|
if non_answers.iter().any(|n| lower == *n) {
|
||||||
|
return Err(format!(
|
||||||
|
"{tool_name} 'reason' '{trimmed}' is too generic — \
|
||||||
|
describe what changes and why (e.g. 'switch to Result<T> for \
|
||||||
|
safer error propagation per user request')"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the textual content of a write/edit call, if any.
|
||||||
|
fn extract_content(tool_name: &str, args: &serde_json::Value) -> Option<String> {
|
||||||
|
match tool_name {
|
||||||
|
"write" => args.get("content").and_then(|v| v.as_str()).map(String::from),
|
||||||
|
"edit" => {
|
||||||
|
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("");
|
||||||
|
Some(format!("{old}\n{new}"))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the first pattern (case-insensitive substring) that matches
|
||||||
|
/// `text`, or `None` if no pattern matched.
|
||||||
|
fn first_match(text: &str, patterns: &'static [&'static str]) -> Option<&'static str> {
|
||||||
|
let lower = text.to_lowercase();
|
||||||
|
let iter: std::slice::Iter<'static, &'static str> = patterns.iter();
|
||||||
|
iter.copied().find(|p| lower.contains(&p.to_lowercase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a candidate output path from a tool call, if one exists.
|
||||||
fn find_output_path(tool_name: &str, args: &serde_json::Value) -> Option<std::path::PathBuf> {
|
fn find_output_path(tool_name: &str, args: &serde_json::Value) -> Option<std::path::PathBuf> {
|
||||||
match tool_name {
|
match tool_name {
|
||||||
"write" | "edit" | "delete" | "read" => {
|
"write" | "edit" | "delete" | "read" => {
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
|||||||
// every ~1s while disconnected, every ~30s while connected, so the
|
// every ~1s while disconnected, every ~30s while connected, so the
|
||||||
// status bar reflects real API availability without user input.
|
// status bar reflects real API availability without user input.
|
||||||
let check_interval = if state.misc.api_connected { 600 } else { 20 };
|
let check_interval = if state.misc.api_connected { 600 } else { 20 };
|
||||||
if state.misc.tick_count % check_interval == 0 {
|
if state.misc.tick_count.is_multiple_of(check_interval) {
|
||||||
spawn_api_connectivity_check(state);
|
spawn_api_connectivity_check(state);
|
||||||
}
|
}
|
||||||
crate::app::review::maybe_run_staleness_sweep(state);
|
crate::app::review::maybe_run_staleness_sweep(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user