Skip to main content

RunLogContext

Struct RunLogContext 

Source
pub struct RunLogContext { /* private fields */ }
Expand description

Context for managing per-run log directories and files.

This struct owns the run_id and provides path resolution for all logs from a single Ralph invocation. All logs are grouped under a per-run directory (.agent/logs-<run_id>/) for easy sharing and diagnosis.

§Design Rationale

Why per-run directories?

  • Shareability: All logs from a run can be shared as a single tarball
  • Resume continuity: --resume continues logging to the same directory
  • Isolation: Multiple concurrent runs don’t interfere with each other
  • Organization: Chronological sorting is natural (lexicographic sort)

Why not scatter logs across .agent/logs/, .agent/tmp/, etc?

  • Hard to identify which logs belong to which run
  • Difficult to share logs for debugging
  • Resume would create fragmented log artifacts
  • Log rotation and cleanup become complex

§Integration with Checkpoint/Resume

The run_id is stored in the checkpoint (.agent/checkpoint.json) so that --resume can continue logging to the same directory. This ensures:

  • Logs from the original run and resumed run are in one place
  • Event loop sequence numbers continue from where they left off
  • Pipeline log is appended (not overwritten)

§Architecture Compliance

This struct is created once per run in the impure layer (effect handlers) and passed to all effect handlers via PhaseContext. It must never be used in reducers or orchestrators (which are pure).

All filesystem operations go through the Workspace trait (never std::fs in pipeline code) to support both WorkspaceFs (production) and MemoryWorkspace (tests).

§Future Extensibility

The per-run directory structure includes reserved subdirectories for future use:

  • provider/: Provider streaming logs (infrastructure exists, not yet used)
  • debug/: Future debug artifacts (e.g., memory dumps, profiling data)

§Examples

§Fresh run

use ralph_workflow::logging::RunLogContext;
use ralph_workflow::workspace::WorkspaceFs;
use std::path::PathBuf;

let workspace = WorkspaceFs::new(PathBuf::from("."));
let ctx = RunLogContext::new(&workspace)?;

// Get log paths
let pipeline_log = ctx.pipeline_log();  // .agent/logs-2026-02-06_14-03-27.123Z/pipeline.log
let agent_log = ctx.agent_log("planning", 1, None);  // .agent/logs-.../agents/planning_1.log

§Resume

use ralph_workflow::logging::RunLogContext;
use ralph_workflow::workspace::WorkspaceFs;
use std::path::PathBuf;

let workspace = WorkspaceFs::new(PathBuf::from("."));
let run_id = "2026-02-06_14-03-27.123Z";  // From checkpoint
let ctx = RunLogContext::from_checkpoint(run_id, &workspace)?;

// Logs will append to existing files in the same run directory
let pipeline_log = ctx.pipeline_log();

Implementations§

Source§

impl RunLogContext

Source

pub fn new(workspace: &dyn Workspace) -> Result<Self>

Create a new RunLogContext with collision handling.

Generates a new run_id and creates the run directory structure. If directory exists, tries collision counter variants (rare case of multiple runs starting in the same millisecond).

Creates subdirectories:

  • .agent/logs-<run_id>/agents/ for per-agent logs
  • .agent/logs-<run_id>/provider/ for provider streaming logs
  • .agent/logs-<run_id>/debug/ for future debug artifacts
§Collision Handling

The collision handling loop tries counter values 0-99:

  • Counter 0: Uses the base run_id (no suffix)
  • Counter 1-99: Appends -01 through -99 suffixes
§TOCTOU Race Condition Handling

To avoid the time-of-check-to-time-of-use race condition, we:

  1. First check if the directory exists (fast path for common case)
  2. If it doesn’t exist, try to create it
  3. If creation succeeds but the directory still doesn’t exist afterward, another process may have created it, so we try the next collision variant
  4. We use the presence of the “agents” subdirectory as our “created” marker

Note: If a base directory exists that was actually created as a collision directory (e.g., due to a bug), the system will still work correctly by creating the next collision variant. This is acceptable because the directory naming format is deterministic and we always check for existence before creating.

Source

pub fn from_checkpoint(run_id: &str, workspace: &dyn Workspace) -> Result<Self>

Create a RunLogContext from an existing checkpoint (for resume).

Uses the timestamp-based log run ID from the checkpoint (stored in PipelineCheckpoint.log_run_id) to continue logging to the same run directory. This is distinct from the UUID-based run_id field in the checkpoint which identifies the execution session.

If the directory doesn’t exist (e.g., deleted), it is recreated.

Source

pub fn for_testing( base_run_id: RunId, workspace: &dyn Workspace, ) -> Result<Self>

Test-only helper to create a RunLogContext with a fixed run_id.

This allows testing the collision handling logic by providing a predictable run_id that can be pre-created on the filesystem to simulate collisions.

§Warning

This is intended for testing only. Using a fixed run_id in production could lead to directory collisions. Always use RunLogContext::new or RunLogContext::from_checkpoint in production code.

§Examples
use ralph_workflow::logging::{RunId, RunLogContext};

// Create a fixed run_id for testing
let fixed_id = RunId::for_test("2026-02-06_14-03-27.123Z");
let ctx = RunLogContext::for_testing(fixed_id, &workspace)?;
Source

pub fn run_id(&self) -> &RunId

Get a reference to the run ID.

This is the timestamp-based log run ID (format: YYYY-MM-DD_HH-mm-ss.SSSZ[-NN]) used for naming the per-run log directory. It is distinct from the UUID-based run_id field stored in PipelineCheckpoint, which uniquely identifies the execution session.

Source

pub fn run_dir(&self) -> &Path

Get the run directory path (relative to workspace root).

Source

pub fn pipeline_log(&self) -> PathBuf

Get the path to the pipeline log file.

Source

pub fn event_loop_log(&self) -> PathBuf

Get the path to the event loop log file.

Source

pub fn event_loop_trace(&self) -> PathBuf

Get the path to the event loop trace file (crash-only).

Source

pub fn agent_log( &self, phase: &str, index: u32, attempt: Option<u32>, ) -> PathBuf

Get the path to an agent log file.

§Arguments
  • phase - Phase name (e.g., “planning”, “dev”, “reviewer”, “commit”)
  • index - Invocation index within the phase (1-based)
  • attempt - Optional retry attempt counter (1 for first retry, 2 for second retry, etc.; None for initial attempt with no retries)
§Returns

Path like .agent/logs-<run_id>/agents/planning_1.log or .agent/logs-<run_id>/agents/dev_2_a1.log for retries.

Source

pub fn provider_log(&self, name: &str) -> PathBuf

Get the path to a provider streaming log file.

§Arguments
  • name - Provider log filename (e.g., “claude-stream_dev_1.jsonl”)
§Returns

Path like .agent/logs-<run_id>/provider/claude-stream_dev_1.jsonl.

Source

pub fn run_metadata(&self) -> PathBuf

Get the path to the run metadata file (run.json).

Source

pub fn write_run_metadata( &self, workspace: &dyn Workspace, metadata: &RunMetadata, ) -> Result<()>

Write run.json metadata file.

This should be called early in pipeline execution to record essential metadata for debugging and tooling.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.