Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added an invariant-checked session snapshot fork operation that rebinds the
session, workspace, run ownership, and subagent parent ownership while
preserving the complete persisted generation.

## [5.3.5] - 2026-07-17

### Added
Expand Down
33 changes: 33 additions & 0 deletions core/src/store/session_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ impl SessionSnapshotV1 {
)
}

/// Rebind a complete snapshot to a new session and workspace.
///
/// Session forks retain historical artifacts, traces, run ids, and child
/// session ids. Top-level run ownership and subagent parent ownership must
/// move with the new session or the aggregate would no longer be loadable.
pub fn fork_for_session(
mut self,
session_id: impl Into<String>,
workspace: impl Into<String>,
) -> Result<Self> {
let source_session_id = self.session.id.clone();
self.validate_for_session(&source_session_id)?;

let session_id = session_id.into();
if session_id.trim().is_empty() {
bail!("forked session id cannot be empty");
}

self.session.id = session_id.clone();
self.session.config.workspace = workspace.into();
for record in &mut self.run_records {
record.snapshot.session_id.clone_from(&session_id);
}
for task in &mut self.subagent_tasks {
if !task.parent_session_id.is_empty() {
task.parent_session_id.clone_from(&session_id);
}
}

self.validate_for_session(&session_id)?;
Ok(self)
}

pub fn artifact_store(&self) -> ArtifactStore {
artifact_store_from(&self.artifacts)
}
Expand Down
25 changes: 25 additions & 0 deletions core/src/store/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ async fn create_test_snapshot() -> SessionSnapshotV1 {
)
}

#[tokio::test]
async fn snapshot_fork_rebinds_every_top_level_session_owner() {
let mut snapshot = create_test_snapshot().await;
for record in &mut snapshot.run_records {
record.snapshot.session_id = snapshot.session.id.clone();
}
snapshot.validate_for_session("test-session-1").unwrap();

let fork = snapshot
.fork_for_session("fork-session", "/tmp/fork-workspace")
.unwrap();

assert_eq!(fork.session.id, "fork-session");
assert_eq!(fork.session.config.workspace, "/tmp/fork-workspace");
assert!(fork
.run_records
.iter()
.all(|record| record.snapshot.session_id == "fork-session"));
assert!(fork
.subagent_tasks
.iter()
.all(|task| task.parent_session_id == "fork-session"));
fork.validate_for_session("fork-session").unwrap();
}

// ========================================================================
// FileSessionStore Tests
// ========================================================================
Expand Down
Loading