diff --git a/apps/arc-web/app/components/tool-use.tsx b/apps/arc-web/app/components/tool-use.tsx
new file mode 100644
index 000000000..7534bfb54
--- /dev/null
+++ b/apps/arc-web/app/components/tool-use.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { ChevronRightIcon } from "@heroicons/react/20/solid";
+import { WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
+
+export interface ToolUse {
+ id: string;
+ toolName: string;
+ input: string;
+ result: string;
+ isError: boolean;
+ durationMs?: number;
+}
+
+export function ToolRow({ tool }: { tool: ToolUse }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
= 60) {
- const hours = Math.floor(minutes / 60);
- const remainMinutes = minutes % 60;
- return `${hours}h ${remainMinutes}m`;
- }
- return `${minutes}m ${seconds}s`;
+function formatDurationMs(ms: number): string {
+ return formatDurationSecs(Math.floor(ms / 1000));
}
-export { formatDuration };
+export { formatDurationMs };
diff --git a/apps/arc-web/app/routes/retros.tsx b/apps/arc-web/app/routes/retros.tsx
index 583e1fd56..8ce54871f 100644
--- a/apps/arc-web/app/routes/retros.tsx
+++ b/apps/arc-web/app/routes/retros.tsx
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useNavigate } from "react-router";
import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
-import { smoothnessConfig, formatDuration } from "../data/retros";
+import { smoothnessConfig, formatDurationMs } from "../data/retros";
import type { SmoothnessRating } from "../data/retros";
import { apiJson } from "../api-client";
import type { PaginatedRetroList } from "@qltysh/arc-api-client";
@@ -142,7 +142,7 @@ export default function Retros({ loaderData }: Route.ComponentProps) {
- {formatDuration(retro.total_duration_ms)}
+ {formatDurationMs(retro.total_duration_ms)}
|
{retro.friction_point_count}
diff --git a/apps/arc-web/app/routes/run-retro.tsx b/apps/arc-web/app/routes/run-retro.tsx
index 7c60bcbf3..1b5e11fcd 100644
--- a/apps/arc-web/app/routes/run-retro.tsx
+++ b/apps/arc-web/app/routes/run-retro.tsx
@@ -4,7 +4,7 @@ import {
learningCategoryConfig,
frictionKindConfig,
openItemKindConfig,
- formatDuration,
+ formatDurationMs,
} from "../data/retros";
import type { Retro } from "../data/retros";
import { apiJson } from "../api-client";
@@ -54,7 +54,7 @@ export default function RunRetro({ loaderData }: Route.ComponentProps) {
{/* Aggregate Stats */}
-
+
0} />
@@ -181,7 +181,7 @@ export default function RunRetro({ loaderData }: Route.ComponentProps) {
|
- {formatDuration(stage.duration_ms)}
+ {formatDurationMs(stage.duration_ms)}
|
0 ? "text-amber" : "text-fg-3"}>
diff --git a/apps/arc-web/app/routes/run-stages.tsx b/apps/arc-web/app/routes/run-stages.tsx
index df5b87c2e..d0fed92b4 100644
--- a/apps/arc-web/app/routes/run-stages.tsx
+++ b/apps/arc-web/app/routes/run-stages.tsx
@@ -2,7 +2,9 @@ import { useState } from "react";
import { Link, useParams } from "react-router";
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
-import { DocumentTextIcon, MapIcon, CommandLineIcon, ChatBubbleLeftIcon, WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
+import { DocumentTextIcon, MapIcon, CommandLineIcon, ChatBubbleLeftIcon } from "@heroicons/react/24/outline";
+import { ToolRow, ToolBlock } from "../components/tool-use";
+import type { ToolUse } from "../components/tool-use";
import { apiJson } from "../api-client";
import { formatDurationSecs } from "../lib/format";
import type { PaginatedRunStageList, StageTurn as ApiStageTurn, PaginatedStageTurnList } from "@qltysh/arc-api-client";
@@ -46,15 +48,6 @@ const statusConfig: Record
-
- {open && (
-
-
-
- Result
- {tool.result}
-
-
- )}
-
- );
-}
-
-function ToolBlock({ tools }: { tools: ToolUse[] }) {
- return (
-
- {tools.map((tool) => (
-
- ))}
-
- );
-}
-
function SystemBlock({ content }: { content: string }) {
return (
diff --git a/apps/arc-web/app/routes/session-detail.tsx b/apps/arc-web/app/routes/session-detail.tsx
index 102e6ffd0..95057dbb5 100644
--- a/apps/arc-web/app/routes/session-detail.tsx
+++ b/apps/arc-web/app/routes/session-detail.tsx
@@ -1,14 +1,14 @@
import { useState } from "react";
import { Link, useParams } from "react-router";
-import { ChevronRightIcon } from "@heroicons/react/20/solid";
import {
ChatBubbleLeftIcon,
ClipboardDocumentIcon,
CheckIcon,
PencilSquareIcon,
UserIcon,
- WrenchScrewdriverIcon,
} from "@heroicons/react/24/outline";
+import { ToolRow, ToolBlock } from "../components/tool-use";
+import type { ToolUse } from "../components/tool-use";
import { timeAgo, groupSessionsByDate } from "../lib/time";
import { apiJson } from "../api-client";
import type { SessionDetail as ApiSessionDetail, PaginatedSessionList } from "@qltysh/arc-api-client";
@@ -58,15 +58,6 @@ export async function loader({ request, params }: Route.LoaderArgs) {
return { session, sessionGroups };
}
-interface ToolUse {
- id: string;
- toolName: string;
- input: string;
- result: string;
- isError: boolean;
- durationMs?: number;
-}
-
type Turn =
| { kind: "user"; content: string; created_at?: string }
| { kind: "assistant"; content: string }
@@ -294,48 +285,6 @@ const sessionGroups: SessionGroupType[] = [
},
];
-function ToolRow({ tool }: { tool: ToolUse }) {
- const [open, setOpen] = useState(false);
-
- return (
-
-
- {open && (
-
-
-
- Result
- {tool.result}
-
-
- )}
-
- );
-}
-
-function ToolBlock({ tools }: { tools: ToolUse[] }) {
- return (
-
- {tools.map((tool) => (
-
- ))}
-
- );
-}
-
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs
index 4a4be8ea1..d6ee64b95 100644
--- a/crates/arc-api/src/server.rs
+++ b/crates/arc-api/src/server.rs
@@ -868,22 +868,7 @@ async fn cancel_run(
let mut runs = state.runs.lock().expect("runs lock poisoned");
match runs.get_mut(&id) {
Some(managed_run) => match managed_run.status {
- RunStatus::Queued => {
- managed_run.status = RunStatus::Cancelled;
- let created_at = managed_run.created_at;
- (
- StatusCode::OK,
- Json(RunStatusResponse {
- id: id.clone(),
- status: RunStatus::Cancelled,
- error: None,
- queue_position: None,
- created_at,
- }),
- )
- .into_response()
- }
- RunStatus::Starting | RunStatus::Running => {
+ RunStatus::Queued | RunStatus::Starting | RunStatus::Running => {
if let Some(token) = &managed_run.cancel_token {
token.store(true, Ordering::Relaxed);
}
diff --git a/crates/arc-workflows/src/checkpoint.rs b/crates/arc-workflows/src/checkpoint.rs
index f9e541413..d3df58f2a 100644
--- a/crates/arc-workflows/src/checkpoint.rs
+++ b/crates/arc-workflows/src/checkpoint.rs
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::context::Context;
-use crate::error::{ArcError, FailureSignature, Result};
+use crate::error::{FailureSignature, Result};
use crate::outcome::Outcome;
/// Serializable snapshot of execution state for crash recovery and resume.
@@ -70,10 +70,7 @@ impl Checkpoint {
/// Returns an error if serialization or file writing fails.
pub fn save(&self, path: &Path) -> Result<()> {
tracing::debug!(path = %path.display(), node = %self.current_node, "Saving checkpoint");
- let json = serde_json::to_string_pretty(self)
- .map_err(|e| ArcError::Checkpoint(format!("serialize failed: {e}")))?;
- std::fs::write(path, json)?;
- Ok(())
+ crate::save_json(self, path, "checkpoint")
}
/// Load a checkpoint from a JSON file.
@@ -83,10 +80,7 @@ impl Checkpoint {
/// Returns an error if the file cannot be read or deserialization fails.
pub fn load(path: &Path) -> Result {
tracing::debug!(path = %path.display(), "Loading checkpoint");
- let data = std::fs::read_to_string(path)?;
- let checkpoint: Self = serde_json::from_str(&data)
- .map_err(|e| ArcError::Checkpoint(format!("deserialize failed: {e}")))?;
- Ok(checkpoint)
+ crate::load_json(path, "checkpoint")
}
}
diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs
index 2ceb201be..c4124b289 100644
--- a/crates/arc-workflows/src/cli/run.rs
+++ b/crates/arc-workflows/src/cli/run.rs
@@ -709,8 +709,8 @@ pub async fn run_command(
{
let (status, failure_reason) = match &engine_result {
- Ok(o) => (o.status.to_string(), o.failure_reason().map(String::from)),
- Err(e) => ("fail".to_string(), Some(e.to_string())),
+ Ok(o) => (o.status.clone(), o.failure_reason().map(String::from)),
+ Err(e) => (crate::outcome::StageStatus::Fail, Some(e.to_string())),
};
let conclusion = crate::conclusion::Conclusion {
timestamp: Utc::now(),
diff --git a/crates/arc-workflows/src/cli/runs.rs b/crates/arc-workflows/src/cli/runs.rs
index 689dbda21..879d52d95 100644
--- a/crates/arc-workflows/src/cli/runs.rs
+++ b/crates/arc-workflows/src/cli/runs.rs
@@ -7,8 +7,8 @@ use serde::Serialize;
use tracing::{debug, info};
#[derive(Args)]
-pub struct RunsListArgs {
- /// Only show runs started before this date (YYYY-MM-DD prefix match)
+pub struct RunFilterArgs {
+ /// Only include runs started before this date (YYYY-MM-DD prefix match)
#[arg(long)]
pub before: Option,
@@ -23,6 +23,12 @@ pub struct RunsListArgs {
/// Include orphan directories (no manifest.json)
#[arg(long)]
pub orphans: bool,
+}
+
+#[derive(Args)]
+pub struct RunsListArgs {
+ #[command(flatten)]
+ pub filter: RunFilterArgs,
/// Output as JSON
#[arg(long)]
@@ -31,21 +37,8 @@ pub struct RunsListArgs {
#[derive(Args)]
pub struct RunsPruneArgs {
- /// Only prune runs started before this date (YYYY-MM-DD prefix match)
- #[arg(long)]
- pub before: Option,
-
- /// Filter by workflow name (substring match)
- #[arg(long)]
- pub workflow: Option,
-
- /// Filter by label (KEY=VALUE, repeatable, AND semantics)
- #[arg(long = "label", value_name = "KEY=VALUE")]
- pub label: Vec,
-
- /// Include orphan directories (no manifest.json)
- #[arg(long)]
- pub orphans: bool,
+ #[command(flatten)]
+ pub filter: RunFilterArgs,
/// Actually delete (default is dry-run)
#[arg(long)]
@@ -87,9 +80,8 @@ pub fn scan_runs(base: &Path) -> Result> {
debug!(dir = %dir_name, "scanning run directory");
let manifest_path = path.join("manifest.json");
- if manifest_path.exists() {
+ if let Ok(manifest) = crate::manifest::Manifest::load(&manifest_path) {
debug!(dir = %dir_name, "reading manifest");
- let manifest = crate::manifest::Manifest::load(&manifest_path)?;
let run_id = manifest.run_id;
let workflow_name = manifest.workflow_name;
@@ -139,17 +131,13 @@ pub fn scan_runs(base: &Path) -> Result> {
}
fn read_status(run_dir: &Path) -> String {
- let conclusion_path = run_dir.join("conclusion.json");
- if conclusion_path.exists() {
- if let Ok(conclusion) = crate::conclusion::Conclusion::load(&conclusion_path) {
- return conclusion.status;
- }
- "unknown".to_string()
- } else if run_dir.join("run.pid").exists() {
- "running".to_string()
- } else {
- "unknown".to_string()
+ if let Ok(conclusion) = crate::conclusion::Conclusion::load(&run_dir.join("conclusion.json")) {
+ return conclusion.status.to_string();
}
+ if run_dir.join("run.pid").exists() {
+ return "running".to_string();
+ }
+ "unknown".to_string()
}
/// Filter runs by criteria. Orphans are excluded unless `include_orphans` is true.
@@ -205,13 +193,13 @@ fn default_logs_base() -> PathBuf {
pub fn list_command(args: &RunsListArgs) -> Result<()> {
let base = default_logs_base();
let runs = scan_runs(&base)?;
- let label_filters = parse_label_filters(&args.label);
+ let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_runs(
&runs,
- args.before.as_deref(),
- args.workflow.as_deref(),
+ args.filter.before.as_deref(),
+ args.filter.workflow.as_deref(),
&label_filters,
- args.orphans,
+ args.filter.orphans,
);
if args.json {
@@ -265,13 +253,13 @@ pub fn prune_command(args: &RunsPruneArgs) -> Result<()> {
pub fn prune_from(args: &RunsPruneArgs, base: &Path) -> Result<()> {
let runs = scan_runs(base)?;
- let label_filters = parse_label_filters(&args.label);
+ let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_runs(
&runs,
- args.before.as_deref(),
- args.workflow.as_deref(),
+ args.filter.before.as_deref(),
+ args.filter.workflow.as_deref(),
&label_filters,
- args.orphans,
+ args.filter.orphans,
);
if filtered.is_empty() {
@@ -540,10 +528,12 @@ mod tests {
);
let args = RunsPruneArgs {
- before: Some("2026-01-01".into()),
- workflow: None,
- label: Vec::new(),
- orphans: false,
+ filter: RunFilterArgs {
+ before: Some("2026-01-01".into()),
+ workflow: None,
+ label: Vec::new(),
+ orphans: false,
+ },
yes: false,
};
@@ -588,10 +578,12 @@ mod tests {
);
let args = RunsPruneArgs {
- before: Some("2026-01-01".into()),
- workflow: None,
- label: Vec::new(),
- orphans: false,
+ filter: RunFilterArgs {
+ before: Some("2026-01-01".into()),
+ workflow: None,
+ label: Vec::new(),
+ orphans: false,
+ },
yes: true,
};
@@ -611,10 +603,12 @@ mod tests {
let orphan_dir = make_run_dir(base, "orphan-dir", None, None, false);
let args = RunsPruneArgs {
- before: None,
- workflow: None,
- label: Vec::new(),
- orphans: true,
+ filter: RunFilterArgs {
+ before: None,
+ workflow: None,
+ label: Vec::new(),
+ orphans: true,
+ },
yes: true,
};
diff --git a/crates/arc-workflows/src/conclusion.rs b/crates/arc-workflows/src/conclusion.rs
index e4bcfa927..d18637527 100644
--- a/crates/arc-workflows/src/conclusion.rs
+++ b/crates/arc-workflows/src/conclusion.rs
@@ -3,12 +3,13 @@ use std::path::Path;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-use crate::error::{ArcError, Result};
+use crate::error::Result;
+use crate::outcome::StageStatus;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conclusion {
pub timestamp: DateTime,
- pub status: String,
+ pub status: StageStatus,
pub duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_reason: Option,
@@ -18,17 +19,11 @@ pub struct Conclusion {
impl Conclusion {
pub fn save(&self, path: &Path) -> Result<()> {
- let json = serde_json::to_string_pretty(self)
- .map_err(|e| ArcError::Checkpoint(format!("conclusion serialize failed: {e}")))?;
- std::fs::write(path, json)?;
- Ok(())
+ crate::save_json(self, path, "conclusion")
}
pub fn load(path: &Path) -> Result {
- let data = std::fs::read_to_string(path)?;
- let conclusion: Self = serde_json::from_str(&data)
- .map_err(|e| ArcError::Checkpoint(format!("conclusion deserialize failed: {e}")))?;
- Ok(conclusion)
+ crate::load_json(path, "conclusion")
}
}
@@ -39,7 +34,7 @@ mod tests {
fn sample_conclusion() -> Conclusion {
Conclusion {
timestamp: Utc::now(),
- status: "success".to_string(),
+ status: crate::outcome::StageStatus::Success,
duration_ms: 12345,
failure_reason: None,
final_git_commit_sha: Some("deadbeef".to_string()),
@@ -55,7 +50,7 @@ mod tests {
conclusion.save(&path).unwrap();
let loaded = Conclusion::load(&path).unwrap();
- assert_eq!(loaded.status, "success");
+ assert_eq!(loaded.status, crate::outcome::StageStatus::Success);
assert_eq!(loaded.duration_ms, 12345);
assert!(loaded.failure_reason.is_none());
assert_eq!(
@@ -87,7 +82,7 @@ mod tests {
let conclusion = Conclusion {
timestamp: Utc::now(),
- status: "fail".to_string(),
+ status: crate::outcome::StageStatus::Fail,
duration_ms: 500,
failure_reason: None,
final_git_commit_sha: None,
@@ -107,7 +102,7 @@ mod tests {
let conclusion = Conclusion {
timestamp: Utc::now(),
- status: "fail".to_string(),
+ status: crate::outcome::StageStatus::Fail,
duration_ms: 100,
failure_reason: Some("timeout".to_string()),
final_git_commit_sha: None,
diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs
index 16d33b351..fa48f3c4c 100644
--- a/crates/arc-workflows/src/engine.rs
+++ b/crates/arc-workflows/src/engine.rs
@@ -26,13 +26,9 @@ use crate::handler::{EngineServices, HandlerRegistry};
use crate::hook::{HookContext, HookDecision, HookEvent, HookRunner};
use crate::interviewer::Interviewer;
use crate::outcome::{Outcome, StageStatus};
+use crate::millis_u64;
use crate::preamble::build_preamble;
-/// Convert a Duration's milliseconds to u64, saturating on overflow.
-fn millis_u64(d: std::time::Duration) -> u64 {
- u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
-}
-
/// Classify the failure mode of a completed outcome.
///
/// Returns `None` for `Success`, `PartialSuccess`, and `Skipped` outcomes.
diff --git a/crates/arc-workflows/src/handler/human.rs b/crates/arc-workflows/src/handler/human.rs
index e0831052b..8a6b6bb0b 100644
--- a/crates/arc-workflows/src/handler/human.rs
+++ b/crates/arc-workflows/src/handler/human.rs
@@ -4,6 +4,7 @@ use std::time::Instant;
use async_trait::async_trait;
+use crate::millis_u64;
use crate::context::keys;
use crate::context::Context;
use crate::error::ArcError;
@@ -16,11 +17,6 @@ use crate::outcome::Outcome;
use super::{EngineServices, Handler};
-/// Convert a Duration's milliseconds to u64, saturating on overflow.
-fn millis_u64(d: std::time::Duration) -> u64 {
- u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
-}
-
/// A choice derived from an outgoing edge.
struct Choice {
key: String,
diff --git a/crates/arc-workflows/src/handler/parallel.rs b/crates/arc-workflows/src/handler/parallel.rs
index 3d5081f5f..d5d9a1818 100644
--- a/crates/arc-workflows/src/handler/parallel.rs
+++ b/crates/arc-workflows/src/handler/parallel.rs
@@ -6,6 +6,7 @@ use arc_agent::Sandbox;
use async_trait::async_trait;
use tokio::sync::Semaphore;
+use crate::millis_u64;
use crate::context::keys;
use crate::context::Context;
use crate::engine::GitCheckpointMode;
@@ -104,11 +105,6 @@ impl Sandbox for WorktreeSandbox {
}
}
-/// Convert a Duration's milliseconds to u64, saturating on overflow.
-fn millis_u64(d: std::time::Duration) -> u64 {
- u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
-}
-
/// Fans out execution to multiple branches concurrently.
/// Each branch gets an isolated context clone and runs independently.
pub struct ParallelHandler;
diff --git a/crates/arc-workflows/src/lib.rs b/crates/arc-workflows/src/lib.rs
index 6e1ea6d42..6affde403 100644
--- a/crates/arc-workflows/src/lib.rs
+++ b/crates/arc-workflows/src/lib.rs
@@ -1,3 +1,30 @@
+/// Convert a Duration's milliseconds to u64, saturating on overflow.
+pub(crate) fn millis_u64(d: std::time::Duration) -> u64 {
+ u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
+}
+
+/// Save a value as pretty-printed JSON to a file.
+pub(crate) fn save_json(
+ value: &T,
+ path: &std::path::Path,
+ label: &str,
+) -> error::Result<()> {
+ let json = serde_json::to_string_pretty(value)
+ .map_err(|e| error::ArcError::Checkpoint(format!("{label} serialize failed: {e}")))?;
+ std::fs::write(path, json)?;
+ Ok(())
+}
+
+/// Load a value from a JSON file.
+pub(crate) fn load_json(
+ path: &std::path::Path,
+ label: &str,
+) -> error::Result {
+ let data = std::fs::read_to_string(path)?;
+ serde_json::from_str(&data)
+ .map_err(|e| error::ArcError::Checkpoint(format!("{label} deserialize failed: {e}")))
+}
+
pub mod artifact;
pub mod asset_snapshot;
pub mod checkpoint;
diff --git a/crates/arc-workflows/src/manifest.rs b/crates/arc-workflows/src/manifest.rs
index 8a7d4c74e..8ad14f40e 100644
--- a/crates/arc-workflows/src/manifest.rs
+++ b/crates/arc-workflows/src/manifest.rs
@@ -4,7 +4,7 @@ use std::path::Path;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
-use crate::error::{ArcError, Result};
+use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
@@ -24,17 +24,11 @@ pub struct Manifest {
impl Manifest {
pub fn save(&self, path: &Path) -> Result<()> {
- let json = serde_json::to_string_pretty(self)
- .map_err(|e| ArcError::Checkpoint(format!("manifest serialize failed: {e}")))?;
- std::fs::write(path, json)?;
- Ok(())
+ crate::save_json(self, path, "manifest")
}
pub fn load(path: &Path) -> Result {
- let data = std::fs::read_to_string(path)?;
- let manifest: Self = serde_json::from_str(&data)
- .map_err(|e| ArcError::Checkpoint(format!("manifest deserialize failed: {e}")))?;
- Ok(manifest)
+ crate::load_json(path, "manifest")
}
}
diff --git a/crates/arc-workflows/src/retro.rs b/crates/arc-workflows/src/retro.rs
index b9eba9317..b8f283e9e 100644
--- a/crates/arc-workflows/src/retro.rs
+++ b/crates/arc-workflows/src/retro.rs
@@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::checkpoint::Checkpoint;
-use crate::error::{ArcError, Result};
+use crate::error::Result;
use crate::outcome::StageStatus;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -170,18 +170,12 @@ impl Retro {
/// Save the retro as JSON to `logs_root/retro.json`.
pub fn save(&self, logs_root: &Path) -> Result<()> {
- let json = serde_json::to_string_pretty(self)
- .map_err(|e| ArcError::Checkpoint(format!("retro serialize failed: {e}")))?;
- std::fs::write(logs_root.join("retro.json"), json)?;
- Ok(())
+ crate::save_json(self, &logs_root.join("retro.json"), "retro")
}
/// Load a retro from `logs_root/retro.json`.
pub fn load(logs_root: &Path) -> Result {
- let data = std::fs::read_to_string(logs_root.join("retro.json"))?;
- let retro: Self = serde_json::from_str(&data)
- .map_err(|e| ArcError::Checkpoint(format!("retro deserialize failed: {e}")))?;
- Ok(retro)
+ crate::load_json(&logs_root.join("retro.json"), "retro")
}
}
|