1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0
use thiserror::Error;
/// Errors that can occur inside the scheduler subsystem.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SchedulerError {
/// The provided cron expression could not be parsed.
///
/// The inner string contains the original expression and the parser's error message.
#[error("invalid cron expression: {0}")]
InvalidCron(String),
/// A low-level `SQLx` error occurred during a database operation.
#[error("sqlx error: {0}")]
Database(#[from] zeph_db::SqlxError),
/// A high-level `zeph-db` error occurred (e.g. during migrations or connection setup).
#[error("db error: {0}")]
Db(#[from] zeph_db::DbError),
/// The [`crate::TaskHandler`] returned an error during task execution.
///
/// The inner string is the human-readable description from the handler.
#[error("task execution failed: {0}")]
TaskFailed(String),
/// A job with the given name already exists in the store.
///
/// Returned by [`crate::JobStore::insert_job`] on a UNIQUE constraint violation.
#[error("job '{0}' already exists")]
DuplicateJob(String),
/// Another `zeph serve` instance is already running with the given PID.
///
/// Returned by [`crate::PidFile::acquire`] when the pid file is locked by another process.
#[cfg(unix)]
#[error(
"daemon pid file is locked: another zeph serve instance appears to be running (pid {pid})"
)]
AlreadyRunning {
/// PID of the running daemon, as stored in the pid file.
pid: u32,
},
/// Failed to detach the daemon process (fork, exec, or I/O redirection error).
#[cfg(unix)]
#[error("daemon detach failed: {0}")]
Detach(String),
/// A generic I/O error from daemon lifecycle operations (pid file, log file).
#[cfg(unix)]
#[error("daemon I/O error: {0}")]
Io(String),
/// A task prompt matched an injection pattern and was blocked by the RTW-A defense.
///
/// The task is skipped for this tick and logged at `WARN` level. No prompt is
/// forwarded to the agent loop.
#[error("prompt injection blocked in task '{task_name}': {reason}")]
PromptInjectionBlocked {
/// Name of the task whose prompt was blocked.
task_name: String,
/// Description of the pattern that triggered the block.
reason: String,
},
/// A task was quarantined by the RTW-A write-fence and skipped for this tick.
///
/// Tasks written to the store in the same tick they would execute are held back
/// for one tick to prevent write-before-exposed-read re-entry attacks.
#[error("task '{task_name}' quarantined by write-fence (written this tick)")]
TaskQuarantined {
/// Name of the task that was quarantined.
task_name: String,
},
}
#[cfg(test)]
mod tests {
use super::SchedulerError;
#[test]
fn database_variant_display() {
let inner = zeph_db::SqlxError::RowNotFound;
let err = SchedulerError::Database(inner);
assert!(
err.to_string().starts_with("sqlx error:"),
"unexpected display: {err}"
);
}
#[test]
fn db_variant_display() {
let inner = zeph_db::DbError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
let err = SchedulerError::Db(inner);
assert!(
err.to_string().starts_with("db error:"),
"unexpected display: {err}"
);
}
}