Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/OPENAGENT_ENGINEERING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ The initial tool set covers directory listing, bounded file reads, text search,

Stage 1 is complete. The bounded agent loop is connected to installed Nemotron 3.5 Lightning packages, uses the selected model ID, retains compatible lower-memory fallbacks, and runs inside the attached-root file boundary.

Stage 2 is in progress. OpenAgent now persists run state, ordered tool steps, bounded results, validation state, and before/after checkpoints. File-tool checkpoints include bounded file contents plus SHA-256 evidence and block destructive mutations that exceed the entry or byte limits. Completed mutations can be restored only after the matching after-state passes conflict, digest, symlink, and attached-root checks; active runs cannot be restored. Startup recovery marks abandoned runs as interrupted, and typed desktop APIs expose run history, checkpoint IDs, step details, and restore results. Automatic interrupted-run replay, terminal-command snapshots, token/runtime metrics, and the visible timeline UI are still pending and must not be represented as complete. The OS-level process sandbox remains Stage 3.
Stage 2 is in progress. OpenAgent now persists run state, ordered tool steps, bounded results, validation state, before/after checkpoints, and restore audit events. File-tool checkpoints include bounded file contents plus SHA-256 evidence and block destructive mutations that exceed the entry or byte limits. Completed mutations can be restored only after the matching after-state passes conflict, digest, symlink, and attached-root checks; active runs cannot be restored. Restore payloads are verified before mutation, failed restores attempt a compensating rollback to the captured after-state, and restored runs return to a validation-required state. Startup recovery marks abandoned runs as interrupted, blocks their active steps, and flags unfinished restore events for operator verification. Typed desktop APIs expose run history, checkpoint IDs, step details, restore results, and restore-event status. When terminal access is disabled, a mutated run reports that required validation was not run instead of presenting an unqualified success. True filesystem transactions, handle-based protection against every symlink race, automatic interrupted-run replay, terminal-command snapshots, token/runtime metrics, and the visible timeline UI are still pending and must not be represented as complete. The OS-level process sandbox remains Stage 3.
17 changes: 17 additions & 0 deletions src-tauri/migrations/008_openagent_restore_audit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE openagent_restore_events (
id TEXT PRIMARY KEY,
checkpoint_id TEXT NOT NULL,
run_id TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('running', 'completed', 'rolled_back', 'rollback_failed')),
restored_files INTEGER NOT NULL DEFAULT 0,
restored_directories INTEGER NOT NULL DEFAULT 0,
removed_paths INTEGER NOT NULL DEFAULT 0,
error TEXT,
started_at TEXT NOT NULL,
completed_at TEXT,
FOREIGN KEY(checkpoint_id) REFERENCES openagent_checkpoints(id) ON DELETE CASCADE,
FOREIGN KEY(run_id) REFERENCES openagent_runs(id) ON DELETE CASCADE
);

CREATE INDEX idx_openagent_restore_events_run_started
ON openagent_restore_events(run_id, started_at DESC);
57 changes: 57 additions & 0 deletions src-tauri/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const MIGRATIONS: &[Migration] = &[
name: "007_openagent_durable_runs",
sql: include_str!("../migrations/007_openagent_durable_runs.sql"),
},
Migration {
number: 8,
name: "008_openagent_restore_audit",
sql: include_str!("../migrations/008_openagent_restore_audit.sql"),
},
];

pub struct Database {
Expand All @@ -82,6 +87,8 @@ impl Database {
database.migrate()?;
database.recover_interrupted_chat_messages()?;
database.recover_interrupted_openagent_runs()?;
database.recover_interrupted_openagent_steps()?;
database.recover_interrupted_restore_events()?;
database.ensure_local_profile()?;
Ok(database)
}
Expand Down Expand Up @@ -183,6 +190,34 @@ impl Database {
.map_err(AppError::from)
}

fn recover_interrupted_openagent_steps(&self) -> Result<usize, AppError> {
let now = Utc::now().to_rfc3339();
self.connection
.execute(
"UPDATE openagent_steps
SET status = 'blocked', error = 'Application exited while this step was active',
completed_at = ?1
WHERE status = 'running'
AND run_id IN (SELECT id FROM openagent_runs WHERE status = 'interrupted')",
params![now],
)
.map_err(AppError::from)
}

fn recover_interrupted_restore_events(&self) -> Result<usize, AppError> {
let now = Utc::now().to_rfc3339();
self.connection
.execute(
"UPDATE openagent_restore_events
SET status = 'rollback_failed',
error = 'Application exited during restore; workspace verification required',
completed_at = ?1
WHERE status = 'running'",
params![now],
)
.map_err(AppError::from)
}

/// Records the schema version this build understands. Kept independent
/// from `app_version` (Cargo package version) — application releases and
/// database schema revisions evolve on different timelines.
Expand Down Expand Up @@ -406,6 +441,7 @@ mod tests {
let message_id = Uuid::new_v4().to_string();
let model_id = Uuid::new_v4().to_string();
let run_id = Uuid::new_v4().to_string();
let step_id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();

{
Expand Down Expand Up @@ -447,6 +483,14 @@ mod tests {
VALUES (?1, ?2, ?3, ?4, ?5, 'test', 'running', 28, ?6, ?6)",
params![run_id, conversation_id, project_id, message_id, model_id, now],
).unwrap();
connection
.execute(
"INSERT INTO openagent_steps
(id, run_id, step_index, tool, action_json, status, started_at)
VALUES (?1, ?2, 1, 'write_file', '{}', 'running', ?3)",
params![step_id, run_id, now],
)
.unwrap();
}

let database = Database::open(path).unwrap();
Expand All @@ -460,6 +504,19 @@ mod tests {
.unwrap();
assert_eq!(status, "interrupted");
assert!(completed_at.is_some());

let (step_status, step_error, step_completed_at): (String, Option<String>, Option<String>) =
database
.connection()
.query_row(
"SELECT status, error, completed_at FROM openagent_steps WHERE id = ?1",
params![step_id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
assert_eq!(step_status, "blocked");
assert!(step_error.unwrap().contains("exited"));
assert!(step_completed_at.is_some());
}

#[test]
Expand Down
Loading
Loading