mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d531adaf18 | ||
|
|
302fa8a38d | ||
|
|
1b9a8ad1b3 | ||
|
|
8c1553e2c9 |
+209
-2
@@ -25,6 +25,7 @@ use crate::agent::routine::{
|
||||
};
|
||||
use crate::channels::{IncomingMessage, OutgoingResponse};
|
||||
use crate::config::RoutineConfig;
|
||||
use crate::context::JobState;
|
||||
use crate::db::Database;
|
||||
use crate::error::RoutineError;
|
||||
use crate::llm::{ChatMessage, CompletionRequest, FinishReason, LlmProvider};
|
||||
@@ -180,6 +181,130 @@ impl RoutineEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync dispatched routine runs with their linked background job status.
|
||||
///
|
||||
/// Full-job routines are fire-and-forget: the routine run is created with
|
||||
/// `Running` status when the job is dispatched, but the run record is never
|
||||
/// updated when the background job completes or fails. This method checks
|
||||
/// all `Running` routine runs that have a linked job, queries the job's
|
||||
/// current state, and updates the routine run accordingly. It also sends
|
||||
/// failure/success notifications that would otherwise be lost.
|
||||
pub async fn sync_dispatched_runs(&self) {
|
||||
let runs = match self.store.list_dispatched_routine_runs().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
tracing::debug!("Failed to list dispatched routine runs: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for run in runs {
|
||||
let Some(job_id) = run.job_id else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Check the linked job's current state
|
||||
let job = match self.store.get_job(job_id).await {
|
||||
Ok(Some(j)) => j,
|
||||
Ok(None) => {
|
||||
// Job was deleted — mark the routine run as failed
|
||||
tracing::warn!(
|
||||
run_id = %run.id,
|
||||
job_id = %job_id,
|
||||
"Linked job not found, marking routine run as failed"
|
||||
);
|
||||
self.complete_dispatched_run(
|
||||
&run,
|
||||
RunStatus::Failed,
|
||||
"Linked job not found (may have been deleted)",
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
run_id = %run.id,
|
||||
job_id = %job_id,
|
||||
"Failed to query linked job: {}", e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the reason from the most recent state transition
|
||||
let last_reason = job.transitions.last().and_then(|t| t.reason.clone());
|
||||
|
||||
// Map job state to routine run status
|
||||
let (new_status, summary) = match job.state {
|
||||
JobState::Completed | JobState::Submitted | JobState::Accepted => {
|
||||
let summary =
|
||||
last_reason.unwrap_or_else(|| "Job completed successfully".to_string());
|
||||
(RunStatus::Ok, summary)
|
||||
}
|
||||
JobState::Failed => {
|
||||
let summary = last_reason
|
||||
.unwrap_or_else(|| "Job failed (no error message recorded)".to_string());
|
||||
(RunStatus::Failed, summary)
|
||||
}
|
||||
JobState::Cancelled => (RunStatus::Failed, "Job was cancelled".to_string()),
|
||||
// Still in progress — skip
|
||||
JobState::Pending | JobState::InProgress | JobState::Stuck => continue,
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
run_id = %run.id,
|
||||
job_id = %job_id,
|
||||
status = %new_status,
|
||||
"Syncing dispatched routine run with completed job"
|
||||
);
|
||||
|
||||
self.complete_dispatched_run(&run, new_status, &summary)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete a dispatched routine run and send the appropriate notification.
|
||||
async fn complete_dispatched_run(&self, run: &RoutineRun, status: RunStatus, summary: &str) {
|
||||
if let Err(e) = self
|
||||
.store
|
||||
.complete_routine_run(run.id, status, Some(summary), None)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
run_id = %run.id,
|
||||
"Failed to update dispatched routine run: {}", e
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up the routine to get its notify config and name
|
||||
match self.store.get_routine(run.routine_id).await {
|
||||
Ok(Some(routine)) => {
|
||||
send_notification(
|
||||
&self.notify_tx,
|
||||
&routine.notify,
|
||||
&routine.name,
|
||||
status,
|
||||
Some(summary),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::debug!(
|
||||
routine_id = %run.routine_id,
|
||||
"Routine not found for notification (may have been deleted)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
routine_id = %run.routine_id,
|
||||
"Failed to look up routine for notification: {}", e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire a routine manually (from tool call or CLI).
|
||||
///
|
||||
/// Bypasses cooldown checks (those only apply to cron/event triggers).
|
||||
@@ -534,9 +659,10 @@ async fn execute_full_job(
|
||||
);
|
||||
|
||||
let summary = format!(
|
||||
"Dispatched job {job_id} for full execution with tool access (max_iterations: {max_iterations})"
|
||||
"Dispatched job {job_id} for full execution with tool access (max_iterations: {max_iterations}). \
|
||||
Status will be updated when the job completes."
|
||||
);
|
||||
Ok((RunStatus::Ok, Some(summary), None))
|
||||
Ok((RunStatus::Running, Some(summary), None))
|
||||
}
|
||||
|
||||
/// Execute a lightweight routine (single LLM call).
|
||||
@@ -712,6 +838,7 @@ pub fn spawn_cron_ticker(
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
engine.check_cron_triggers().await;
|
||||
engine.sync_dispatched_runs().await;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -756,4 +883,84 @@ mod tests {
|
||||
let _ = status.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_running_status_does_not_notify() {
|
||||
// Running status should not trigger notifications (job still in progress)
|
||||
let config = NotifyConfig {
|
||||
on_success: true,
|
||||
on_failure: true,
|
||||
on_attention: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// RunStatus::Running maps to false in send_notification's match
|
||||
let should_notify = match RunStatus::Running {
|
||||
RunStatus::Ok => config.on_success,
|
||||
RunStatus::Attention => config.on_attention,
|
||||
RunStatus::Failed => config.on_failure,
|
||||
RunStatus::Running => false,
|
||||
};
|
||||
assert!(!should_notify);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_job_dispatch_returns_running_status() {
|
||||
// Verify the status text for Running is "running"
|
||||
assert_eq!(RunStatus::Running.to_string(), "running");
|
||||
}
|
||||
|
||||
/// Regression test for #697: full_job routines were immediately marked Ok
|
||||
/// on dispatch, so failures/completions were never synced back. The fix
|
||||
/// changed dispatch to return Running and added sync_dispatched_runs which
|
||||
/// maps terminal job states to routine run statuses.
|
||||
#[test]
|
||||
fn test_job_state_to_run_status_mapping() {
|
||||
use crate::context::JobState;
|
||||
|
||||
// Helper that replicates the mapping logic from sync_dispatched_runs
|
||||
let map_state = |state: JobState, reason: Option<&str>| -> Option<(RunStatus, String)> {
|
||||
let last_reason = reason.map(|s| s.to_string());
|
||||
match state {
|
||||
JobState::Completed | JobState::Submitted | JobState::Accepted => {
|
||||
let summary =
|
||||
last_reason.unwrap_or_else(|| "Job completed successfully".to_string());
|
||||
Some((RunStatus::Ok, summary))
|
||||
}
|
||||
JobState::Failed => {
|
||||
let summary = last_reason
|
||||
.unwrap_or_else(|| "Job failed (no error message recorded)".to_string());
|
||||
Some((RunStatus::Failed, summary))
|
||||
}
|
||||
JobState::Cancelled => Some((RunStatus::Failed, "Job was cancelled".to_string())),
|
||||
JobState::Pending | JobState::InProgress | JobState::Stuck => None,
|
||||
}
|
||||
};
|
||||
|
||||
// Terminal states produce a status update
|
||||
let (status, _) = map_state(JobState::Completed, None).unwrap();
|
||||
assert_eq!(status, RunStatus::Ok);
|
||||
|
||||
let (status, _) = map_state(JobState::Submitted, None).unwrap();
|
||||
assert_eq!(status, RunStatus::Ok);
|
||||
|
||||
let (status, _) = map_state(JobState::Accepted, None).unwrap();
|
||||
assert_eq!(status, RunStatus::Ok);
|
||||
|
||||
let (status, summary) = map_state(JobState::Failed, Some("OOM killed")).unwrap();
|
||||
assert_eq!(status, RunStatus::Failed);
|
||||
assert_eq!(summary, "OOM killed");
|
||||
|
||||
let (status, summary) = map_state(JobState::Failed, None).unwrap();
|
||||
assert_eq!(status, RunStatus::Failed);
|
||||
assert!(summary.contains("no error message"));
|
||||
|
||||
let (status, _) = map_state(JobState::Cancelled, None).unwrap();
|
||||
assert_eq!(status, RunStatus::Failed);
|
||||
|
||||
// In-progress states should NOT produce a status update (skip)
|
||||
assert!(map_state(JobState::Pending, None).is_none());
|
||||
assert!(map_state(JobState::InProgress, None).is_none());
|
||||
assert!(map_state(JobState::Stuck, None).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,18 +169,10 @@ pub(crate) fn parse_timestamp(s: &str) -> Result<DateTime<Utc>, String> {
|
||||
}
|
||||
// Naive with fractional seconds (legacy or SQLite datetime() output)
|
||||
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
|
||||
tracing::warn!(
|
||||
timestamp = s,
|
||||
"parsing naive timestamp without timezone; assuming UTC — consider re-running migrations"
|
||||
);
|
||||
return Ok(ndt.and_utc());
|
||||
}
|
||||
// Naive without fractional seconds (legacy format)
|
||||
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
|
||||
tracing::warn!(
|
||||
timestamp = s,
|
||||
"parsing naive timestamp without timezone; assuming UTC — consider re-running migrations"
|
||||
);
|
||||
return Ok(ndt.and_utc());
|
||||
}
|
||||
Err(format!("unparseable timestamp: {:?}", s))
|
||||
@@ -518,98 +510,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timestamp_rfc3339() {
|
||||
use super::parse_timestamp;
|
||||
|
||||
// Standard RFC 3339 with Z suffix
|
||||
let dt = parse_timestamp("2024-01-15T10:30:00.123Z").unwrap();
|
||||
assert_eq!(
|
||||
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||
"2024-01-15T10:30:00.123Z"
|
||||
);
|
||||
|
||||
// RFC 3339 with +00:00 offset
|
||||
let dt = parse_timestamp("2024-01-15T10:30:00.000+00:00").unwrap();
|
||||
assert_eq!(
|
||||
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||
"2024-01-15T10:30:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timestamp_naive_fallback() {
|
||||
use super::parse_timestamp;
|
||||
|
||||
// Naive with fractional seconds (legacy datetime('now') output)
|
||||
let dt = parse_timestamp("2024-01-15 10:30:00.123").unwrap();
|
||||
assert_eq!(
|
||||
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||
"2024-01-15T10:30:00.123Z"
|
||||
);
|
||||
|
||||
// Naive without fractional seconds
|
||||
let dt = parse_timestamp("2024-01-15 10:30:00").unwrap();
|
||||
assert_eq!(
|
||||
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||
"2024-01-15T10:30:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timestamp_invalid() {
|
||||
use super::parse_timestamp;
|
||||
|
||||
assert!(parse_timestamp("not-a-timestamp").is_err());
|
||||
assert!(parse_timestamp("").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_timestamps_are_rfc3339() {
|
||||
// Verify that DEFAULT column values produce RFC 3339 timestamps
|
||||
// after the migration change from datetime('now') to strftime.
|
||||
// Use file-based DB because in-memory doesn't share schema across connections.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db_path = dir.path().join("test_ts.db");
|
||||
let backend = LibSqlBackend::new_local(&db_path).await.unwrap();
|
||||
backend.run_migrations().await.unwrap();
|
||||
|
||||
let conn = backend.connect().await.unwrap();
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO conversations (id, channel, user_id) VALUES (?1, ?2, ?3)",
|
||||
libsql::params![id.clone(), "test", "user1"],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut rows = conn
|
||||
.query(
|
||||
"SELECT started_at, last_activity FROM conversations WHERE id = ?1",
|
||||
libsql::params![id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let row = rows.next().await.unwrap().unwrap();
|
||||
let started_at: String = row.get(0).unwrap();
|
||||
let last_activity: String = row.get(1).unwrap();
|
||||
|
||||
// Must end with 'Z' (RFC 3339 UTC) and contain 'T' separator
|
||||
assert!(
|
||||
started_at.ends_with('Z') && started_at.contains('T'),
|
||||
"started_at should be RFC 3339, got: {started_at}"
|
||||
);
|
||||
assert!(
|
||||
last_activity.ends_with('Z') && last_activity.contains('T'),
|
||||
"last_activity should be RFC 3339, got: {last_activity}"
|
||||
);
|
||||
|
||||
// Must be parseable by the RFC 3339 parser directly (not just naive fallback)
|
||||
use chrono::DateTime;
|
||||
assert!(
|
||||
DateTime::parse_from_rfc3339(&started_at).is_ok(),
|
||||
"started_at not valid RFC 3339: {started_at}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,4 +423,29 @@ impl RoutineStore for LibSqlBackend {
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_dispatched_routine_runs(&self) -> Result<Vec<RoutineRun>, DatabaseError> {
|
||||
let conn = self.connect().await?;
|
||||
let mut rows = conn
|
||||
.query(
|
||||
&format!(
|
||||
"SELECT {} FROM routine_runs \
|
||||
WHERE status = 'running' AND job_id IS NOT NULL",
|
||||
ROUTINE_RUN_COLUMNS
|
||||
),
|
||||
params![],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?;
|
||||
|
||||
let mut runs = Vec::new();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
runs.push(row_to_routine_run_libsql(&row)?);
|
||||
}
|
||||
Ok(runs)
|
||||
}
|
||||
}
|
||||
|
||||
+55
-55
@@ -26,7 +26,7 @@ pub const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ==================== Conversations ====================
|
||||
@@ -36,8 +36,8 @@ CREATE TABLE IF NOT EXISTS conversations (
|
||||
channel TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
thread_id TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
last_activity TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
metadata TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS conversation_messages (
|
||||
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_messages_conversation
|
||||
@@ -91,7 +91,7 @@ CREATE TABLE IF NOT EXISTS agent_jobs (
|
||||
failure_reason TEXT,
|
||||
stuck_since TEXT,
|
||||
repair_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
started_at TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
@@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS job_actions (
|
||||
duration_ms INTEGER,
|
||||
success INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(job_id, sequence_num)
|
||||
);
|
||||
|
||||
@@ -137,8 +137,8 @@ CREATE TABLE IF NOT EXISTS dynamic_tools (
|
||||
failure_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dynamic_tools_status ON dynamic_tools(status);
|
||||
@@ -156,7 +156,7 @@ CREATE TABLE IF NOT EXISTS llm_calls (
|
||||
output_tokens INTEGER NOT NULL,
|
||||
cost TEXT NOT NULL,
|
||||
purpose TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_llm_calls_job ON llm_calls(job_id);
|
||||
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS estimation_snapshots (
|
||||
actual_time_secs INTEGER,
|
||||
estimated_value TEXT NOT NULL,
|
||||
actual_value TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_estimation_category ON estimation_snapshots(category);
|
||||
@@ -192,7 +192,7 @@ CREATE TABLE IF NOT EXISTS repair_attempts (
|
||||
action_taken TEXT NOT NULL,
|
||||
success INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_repair_attempts_target ON repair_attempts(target_type, target_id);
|
||||
@@ -206,8 +206,8 @@ CREATE TABLE IF NOT EXISTS memory_documents (
|
||||
agent_id TEXT,
|
||||
path TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
metadata TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (user_id, agent_id, path)
|
||||
);
|
||||
@@ -222,7 +222,7 @@ CREATE TRIGGER IF NOT EXISTS update_memory_documents_updated_at
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE memory_documents SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.id;
|
||||
UPDATE memory_documents SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
-- ==================== Workspace: Memory Chunks ====================
|
||||
@@ -234,7 +234,7 @@ CREATE TABLE IF NOT EXISTS memory_chunks (
|
||||
chunk_index INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
embedding BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (document_id, chunk_index)
|
||||
);
|
||||
|
||||
@@ -296,8 +296,8 @@ CREATE TABLE IF NOT EXISTS secrets (
|
||||
expires_at TEXT,
|
||||
last_used_at TEXT,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
@@ -318,8 +318,8 @@ CREATE TABLE IF NOT EXISTS wasm_tools (
|
||||
source_url TEXT,
|
||||
trust_level TEXT NOT NULL DEFAULT 'user',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, name, version)
|
||||
);
|
||||
|
||||
@@ -340,8 +340,8 @@ CREATE TABLE IF NOT EXISTS wasm_channels (
|
||||
binary_hash BLOB NOT NULL,
|
||||
capabilities_json TEXT NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
@@ -359,8 +359,8 @@ CREATE TABLE IF NOT EXISTS tool_capabilities (
|
||||
max_response_body_bytes INTEGER NOT NULL DEFAULT 10485760,
|
||||
workspace_read_prefixes TEXT NOT NULL DEFAULT '[]',
|
||||
http_timeout_secs INTEGER NOT NULL DEFAULT 30,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (wasm_tool_id)
|
||||
);
|
||||
|
||||
@@ -373,7 +373,7 @@ CREATE TABLE IF NOT EXISTS leak_detection_patterns (
|
||||
severity TEXT NOT NULL DEFAULT 'high',
|
||||
action TEXT NOT NULL DEFAULT 'block',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ==================== Rate Limit State ====================
|
||||
@@ -382,9 +382,9 @@ CREATE TABLE IF NOT EXISTS tool_rate_limit_state (
|
||||
id TEXT PRIMARY KEY,
|
||||
wasm_tool_id TEXT NOT NULL REFERENCES wasm_tools(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL,
|
||||
minute_window_start TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
minute_window_start TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
minute_count INTEGER NOT NULL DEFAULT 0,
|
||||
hour_window_start TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
hour_window_start TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
hour_count INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (wasm_tool_id, user_id)
|
||||
);
|
||||
@@ -400,7 +400,7 @@ CREATE TABLE IF NOT EXISTS secret_usage_log (
|
||||
target_path TEXT,
|
||||
success INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_secret_usage_user ON secret_usage_log(user_id);
|
||||
@@ -415,7 +415,7 @@ CREATE TABLE IF NOT EXISTS leak_detection_events (
|
||||
source TEXT NOT NULL,
|
||||
action_taken TEXT NOT NULL,
|
||||
context_preview TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ==================== Tool Failures ====================
|
||||
@@ -425,8 +425,8 @@ CREATE TABLE IF NOT EXISTS tool_failures (
|
||||
tool_name TEXT NOT NULL UNIQUE,
|
||||
error_message TEXT,
|
||||
error_count INTEGER DEFAULT 1,
|
||||
first_failure TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
last_failure TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
first_failure TEXT DEFAULT (datetime('now')),
|
||||
last_failure TEXT DEFAULT (datetime('now')),
|
||||
last_build_result TEXT,
|
||||
repaired_at TEXT,
|
||||
repair_attempts INTEGER DEFAULT 0
|
||||
@@ -441,7 +441,7 @@ CREATE TABLE IF NOT EXISTS job_events (
|
||||
job_id TEXT NOT NULL REFERENCES agent_jobs(id),
|
||||
event_type TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_job_events_job ON job_events(job_id, id);
|
||||
@@ -471,8 +471,8 @@ CREATE TABLE IF NOT EXISTS routines (
|
||||
next_fire_at TEXT,
|
||||
run_count INTEGER NOT NULL DEFAULT 0,
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
@@ -485,13 +485,13 @@ CREATE TABLE IF NOT EXISTS routine_runs (
|
||||
routine_id TEXT NOT NULL REFERENCES routines(id) ON DELETE CASCADE,
|
||||
trigger_type TEXT NOT NULL,
|
||||
trigger_detail TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
result_summary TEXT,
|
||||
tokens_used INTEGER,
|
||||
job_id TEXT REFERENCES agent_jobs(id),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_routine_runs_routine ON routine_runs(routine_id);
|
||||
@@ -502,7 +502,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
user_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (user_id, key)
|
||||
);
|
||||
|
||||
@@ -558,24 +558,24 @@ CREATE INDEX IF NOT EXISTS idx_heartbeat_next_run ON heartbeat_state(next_run);
|
||||
|
||||
-- Pre-populate leak detection patterns (matches PostgreSQL V2 migration).
|
||||
INSERT OR IGNORE INTO leak_detection_patterns (id, name, pattern, severity, action, enabled, created_at) VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'openai_api_key', 'sk-(?:proj-)?[a-zA-Z0-9]{20,}(?:T3BlbkFJ[a-zA-Z0-9_-]*)?', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'anthropic_api_key', 'sk-ant-api[a-zA-Z0-9_-]{90,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'aws_access_key', 'AKIA[0-9A-Z]{16}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440004', 'aws_secret_key', '(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440005', 'github_token', 'gh[pousr]_[A-Za-z0-9_]{36,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440006', 'github_fine_grained_pat', 'github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440007', 'stripe_api_key', 'sk_(?:live|test)_[a-zA-Z0-9]{24,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440008', 'nearai_session', 'sess_[a-zA-Z0-9]{32,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440009', 'bearer_token', 'Bearer\s+[a-zA-Z0-9_-]{20,}', 'high', 'redact', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000a', 'pem_private_key', '-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000b', 'ssh_private_key', '-----BEGIN\s+(?:OPENSSH|EC|DSA)\s+PRIVATE\s+KEY-----', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000c', 'google_api_key', 'AIza[0-9A-Za-z_-]{35}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000d', 'slack_token', 'xox[baprs]-[0-9a-zA-Z-]{10,}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000e', 'discord_token', '[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000f', 'twilio_api_key', 'SK[a-fA-F0-9]{32}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440010', 'sendgrid_api_key', 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440011', 'mailchimp_api_key', '[a-f0-9]{32}-us[0-9]{1,2}', 'medium', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
('550e8400-e29b-41d4-a716-446655440012', 'high_entropy_hex', '(?<![a-fA-F0-9])[a-fA-F0-9]{64}(?![a-fA-F0-9])', 'medium', 'warn', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'openai_api_key', 'sk-(?:proj-)?[a-zA-Z0-9]{20,}(?:T3BlbkFJ[a-zA-Z0-9_-]*)?', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'anthropic_api_key', 'sk-ant-api[a-zA-Z0-9_-]{90,}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'aws_access_key', 'AKIA[0-9A-Z]{16}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440004', 'aws_secret_key', '(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440005', 'github_token', 'gh[pousr]_[A-Za-z0-9_]{36,}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440006', 'github_fine_grained_pat', 'github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440007', 'stripe_api_key', 'sk_(?:live|test)_[a-zA-Z0-9]{24,}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440008', 'nearai_session', 'sess_[a-zA-Z0-9]{32,}', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440009', 'bearer_token', 'Bearer\s+[a-zA-Z0-9_-]{20,}', 'high', 'redact', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000a', 'pem_private_key', '-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000b', 'ssh_private_key', '-----BEGIN\s+(?:OPENSSH|EC|DSA)\s+PRIVATE\s+KEY-----', 'critical', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000c', 'google_api_key', 'AIza[0-9A-Za-z_-]{35}', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000d', 'slack_token', 'xox[baprs]-[0-9a-zA-Z-]{10,}', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000e', 'discord_token', '[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-44665544000f', 'twilio_api_key', 'SK[a-fA-F0-9]{32}', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440010', 'sendgrid_api_key', 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}', 'high', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440011', 'mailchimp_api_key', '[a-f0-9]{32}-us[0-9]{1,2}', 'medium', 'block', 1, datetime('now')),
|
||||
('550e8400-e29b-41d4-a716-446655440012', 'high_entropy_hex', '(?<![a-fA-F0-9])[a-fA-F0-9]{64}(?![a-fA-F0-9])', 'medium', 'warn', 1, datetime('now'));
|
||||
|
||||
"#;
|
||||
|
||||
@@ -613,7 +613,7 @@ CREATE TABLE IF NOT EXISTS memory_chunks_new (
|
||||
chunk_index INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
embedding BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (document_id, chunk_index)
|
||||
);
|
||||
|
||||
|
||||
@@ -303,6 +303,10 @@ pub trait RoutineStore: Send + Sync {
|
||||
run_id: Uuid,
|
||||
job_id: Uuid,
|
||||
) -> Result<(), DatabaseError>;
|
||||
/// List routine runs that were dispatched as full_job (status = 'running'
|
||||
/// with a linked job_id). Used by the routine engine to sync completion
|
||||
/// status from the background job.
|
||||
async fn list_dispatched_routine_runs(&self) -> Result<Vec<RoutineRun>, DatabaseError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -494,6 +494,10 @@ impl RoutineStore for PgBackend {
|
||||
) -> Result<(), DatabaseError> {
|
||||
self.store.link_routine_run_to_job(run_id, job_id).await
|
||||
}
|
||||
|
||||
async fn list_dispatched_routine_runs(&self) -> Result<Vec<RoutineRun>, DatabaseError> {
|
||||
self.store.list_dispatched_routine_runs().await
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ToolFailureStore ====================
|
||||
|
||||
@@ -1295,6 +1295,17 @@ impl Store {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_dispatched_routine_runs(&self) -> Result<Vec<RoutineRun>, DatabaseError> {
|
||||
let conn = self.conn().await?;
|
||||
let rows = conn
|
||||
.query(
|
||||
"SELECT * FROM routine_runs WHERE status = 'running' AND job_id IS NOT NULL",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
rows.iter().map(row_to_routine_run).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
|
||||
Reference in New Issue
Block a user