use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::db;
use crate::handoff;
use crate::memory::entries as memory_entries;
use crate::memory::file as memory_file;
use crate::paths::state::StateLayout;
use crate::paths::write as path_write;
use crate::state::pod_identity;
use crate::state::session_gates;
use crate::state::text_surface::{
extract_generic_markdown_lines, starts_with_any_prefix_case_insensitive, strip_fenced_blocks,
MarkdownLineExtraction,
};
const CCD_MEMORY_OPENING_FENCE: &str = "```ccd-memory";
const GENERIC_CLOSING_FENCE: &str = "```";
const NATIVE_PROFILE_RUNTIME_SCHEMA_VERSION: u32 = 1;
const NATIVE_REPO_RUNTIME_SCHEMA_VERSION: u32 = 1;
const CHECKPOINT_SCHEMA_VERSION: u32 = 1;
const MAX_CHECKPOINT_SUMMARY_CHARS: usize = 280;
const MAX_RECOVERY_IMMEDIATE_ACTIONS: usize = 8;
const MAX_RECOVERY_KEY_FILES: usize = 8;
const MAX_WORKING_BUFFER_SUMMARY_LINES: usize = 12;
const MAX_WORKING_BUFFER_BYTES: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeLifecycle {
#[serde(rename = "active", alias = "Active")]
Active,
#[serde(rename = "inactive", alias = "Inactive")]
Inactive,
}
impl RuntimeLifecycle {
pub fn is_active(self) -> bool {
matches!(self, Self::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeMemoryOrigin {
Narrative,
Structured { entry_type: String, origin: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeMemoryEntry {
pub id: Option<String>,
pub text: String,
pub lifecycle: RuntimeLifecycle,
pub origin: RuntimeMemoryOrigin,
pub state: Option<String>,
pub created_at: Option<String>,
pub last_touched_session: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub superseded_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decay_class: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
pub tags: Vec<String>,
pub source_ref: Option<String>,
pub supersedes: Vec<String>,
}
impl RuntimeMemoryEntry {
pub fn narrative(text: impl Into<String>) -> Self {
Self {
id: None,
text: text.into(),
lifecycle: RuntimeLifecycle::Active,
origin: RuntimeMemoryOrigin::Narrative,
state: None,
created_at: None,
last_touched_session: None,
superseded_at: None,
decay_class: None,
expires_at: None,
tags: Vec::new(),
source_ref: None,
supersedes: Vec::new(),
}
}
pub fn from_structured_entry(entry: memory_entries::StructuredMemoryEntry) -> Self {
let lifecycle = runtime_memory_lifecycle(&entry.state);
Self {
id: Some(entry.id),
text: entry.content,
lifecycle,
origin: RuntimeMemoryOrigin::Structured {
entry_type: entry.entry_type,
origin: entry.origin,
},
state: Some(entry.state),
created_at: Some(entry.created_at),
last_touched_session: Some(entry.last_touched_session),
superseded_at: entry.superseded_at,
decay_class: entry.decay_class,
expires_at: entry.expires_at,
tags: entry.tags,
source_ref: entry.source_ref,
supersedes: entry.supersedes,
}
}
pub fn projection_text(&self) -> String {
match &self.origin {
RuntimeMemoryOrigin::Narrative => self.text.clone(),
RuntimeMemoryOrigin::Structured { entry_type, .. } => {
format!("{entry_type}: {}", self.text)
}
}
}
pub fn structured_id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn structured_state(&self) -> Option<&str> {
self.state.as_deref()
}
pub fn as_structured_entry(&self) -> Option<memory_entries::StructuredMemoryEntry> {
let RuntimeMemoryOrigin::Structured { entry_type, origin } = &self.origin else {
return None;
};
Some(memory_entries::StructuredMemoryEntry {
id: self.id.clone()?,
entry_type: entry_type.clone(),
state: self.state.clone()?,
created_at: self.created_at.clone()?,
last_touched_session: self.last_touched_session?,
origin: origin.clone(),
superseded_at: self.superseded_at.clone(),
decay_class: self.decay_class.clone(),
expires_at: self.expires_at.clone(),
tags: self.tags.clone(),
source_ref: self.source_ref.clone(),
supersedes: self.supersedes.clone(),
content: self.text.clone(),
})
}
pub fn with_structured_state(&self, state: &str) -> Self {
let mut entry = self.clone();
entry.state = Some(state.to_owned());
entry.lifecycle = runtime_memory_lifecycle(state);
if state != "superseded" {
entry.superseded_at = None;
}
entry
}
pub fn with_source_ref(&self, source_ref: Option<String>) -> Self {
let mut entry = self.clone();
entry.source_ref = source_ref;
entry
}
pub fn with_superseded_at(&self, superseded_at: Option<String>) -> Self {
let mut entry = self.clone();
entry.superseded_at = superseded_at;
entry
}
pub fn structured_decay_class(&self) -> Option<&str> {
self.decay_class.as_deref()
}
pub fn with_decay_class(&self, decay_class: Option<String>) -> Self {
let mut entry = self.clone();
entry.decay_class = decay_class;
entry
}
pub fn with_supersedes(&self, supersedes: Vec<String>) -> Self {
let mut entry = self.clone();
entry.supersedes = supersedes;
entry
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHandoffItem {
pub text: String,
pub lifecycle: RuntimeLifecycle,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeMemoryState {
pub profile: Vec<RuntimeMemoryEntry>,
#[serde(alias = "repo")]
pub locality: Vec<RuntimeMemoryEntry>,
#[serde(default)]
pub pod: Vec<RuntimeMemoryEntry>,
#[serde(default)]
pub branch: Vec<RuntimeMemoryEntry>,
#[serde(default)]
pub clone: Vec<RuntimeMemoryEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeHandoffState {
pub title: String,
pub immediate_actions: Vec<RuntimeHandoffItem>,
pub completed_state: Vec<RuntimeHandoffItem>,
pub operational_guardrails: Vec<RuntimeHandoffItem>,
pub key_files: Vec<RuntimeHandoffItem>,
pub definition_of_done: Vec<RuntimeHandoffItem>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryOrigin {
Compaction,
RiskyPause,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCheckpointState {
pub origin: RecoveryOrigin,
pub captured_at_epoch_s: u64,
pub session_started_at_epoch_s: u64,
pub summary: String,
pub immediate_actions: Vec<String>,
pub key_files: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeWorkingBufferState {
pub origin: RecoveryOrigin,
pub captured_at_epoch_s: u64,
pub session_started_at_epoch_s: u64,
pub summary_lines: Vec<String>,
}
pub type RuntimeMemoryAutomationLedger = BTreeMap<String, RuntimeMemoryAutomationRecord>;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeMemoryAutomationRecord {
pub entry_type: String,
#[serde(default)]
pub observation_count: u64,
#[serde(default)]
pub session_ids: Vec<String>,
#[serde(default)]
pub locality_ids: Vec<String>,
pub first_observed_at_epoch_s: u64,
pub last_observed_at_epoch_s: u64,
#[serde(default)]
pub contradiction_count: u64,
#[serde(default)]
pub has_structural_signal: bool,
}
impl RuntimeMemoryAutomationRecord {
pub fn observed_session_count(&self) -> usize {
self.session_ids.len()
}
pub fn observed_locality_count(&self) -> usize {
self.locality_ids.len()
}
pub fn observe(
&self,
entry_type: &str,
observed_at_epoch_s: u64,
session_id: Option<&str>,
locality_id: Option<&str>,
structural_signal: bool,
) -> Self {
let mut next = if self.observation_count == 0 {
Self {
entry_type: entry_type.to_owned(),
observation_count: 0,
session_ids: Vec::new(),
locality_ids: Vec::new(),
first_observed_at_epoch_s: observed_at_epoch_s,
last_observed_at_epoch_s: observed_at_epoch_s,
contradiction_count: 0,
has_structural_signal: false,
}
} else {
self.clone()
};
if !next.entry_type.is_empty() && next.entry_type != entry_type {
next.contradiction_count = next.contradiction_count.saturating_add(1);
}
next.entry_type = entry_type.to_owned();
next.observation_count = next.observation_count.saturating_add(1);
next.last_observed_at_epoch_s = observed_at_epoch_s;
if next.first_observed_at_epoch_s == 0 {
next.first_observed_at_epoch_s = observed_at_epoch_s;
}
if let Some(session_id) = session_id {
if !next
.session_ids
.iter()
.any(|existing| existing == session_id)
{
next.session_ids.push(session_id.to_owned());
next.session_ids.sort_unstable();
}
}
if let Some(locality_id) = locality_id {
if !next
.locality_ids
.iter()
.any(|existing| existing == locality_id)
{
next.locality_ids.push(locality_id.to_owned());
next.locality_ids.sort_unstable();
}
}
next.has_structural_signal |= structural_signal;
next
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeRecoveryState {
#[serde(skip_serializing_if = "Option::is_none")]
pub checkpoint: Option<RuntimeCheckpointState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_buffer: Option<RuntimeWorkingBufferState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeState {
pub memory: RuntimeMemoryState,
pub handoff: RuntimeHandoffState,
}
#[derive(Debug, Clone)]
pub struct RuntimeTextSurface {
pub kind: &'static str,
pub path: PathBuf,
pub status: RuntimeTextSurfaceStatus,
pub content: String,
pub migrated_from: Option<String>,
}
impl RuntimeTextSurface {
pub fn missing(kind: &'static str, path: &Path) -> Self {
Self {
kind,
path: path.to_path_buf(),
status: RuntimeTextSurfaceStatus::Missing,
content: String::new(),
migrated_from: None,
}
}
pub fn is_missing(&self) -> bool {
self.status.is_missing()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeTextSurfaceStatus {
Missing,
Empty,
Loaded,
LoadedNative,
}
impl RuntimeTextSurfaceStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Missing => "missing",
Self::Empty => "empty",
Self::Loaded => "loaded",
Self::LoadedNative => "loaded_native",
}
}
pub fn is_missing(self) -> bool {
matches!(self, Self::Missing)
}
pub fn is_loaded_native(self) -> bool {
matches!(self, Self::LoadedNative)
}
}
#[derive(Debug, Clone)]
pub struct RuntimeSourceSurfaces {
pub profile_memory: RuntimeTextSurface,
pub locality_memory: RuntimeTextSurface,
pub pod_memory: RuntimeTextSurface,
pub branch_memory: RuntimeTextSurface,
pub clone_memory: RuntimeTextSurface,
pub execution_gates: RuntimeTextSurface,
pub handoff: RuntimeTextSurface,
}
#[derive(Debug, Clone)]
pub struct RuntimeRecoverySurface {
pub kind: &'static str,
pub path: PathBuf,
pub status: &'static str,
}
impl RuntimeRecoverySurface {
fn missing(kind: &'static str, path: &Path) -> Self {
Self {
kind,
path: path.to_path_buf(),
status: "missing",
}
}
}
#[derive(Debug, Clone)]
pub struct RuntimeRecoverySurfaces {
pub checkpoint: RuntimeRecoverySurface,
pub working_buffer: RuntimeRecoverySurface,
}
#[derive(Debug, Clone)]
pub struct LoadedRuntimeState {
pub state: RuntimeState,
pub sources: RuntimeSourceSurfaces,
pub execution_gates: LoadedRuntimeExecutionGates,
pub recovery: LoadedRuntimeRecoveryState,
pub pod_identity_active: bool,
}
#[derive(Debug, Clone)]
pub struct LoadedRuntimeMemorySurface {
pub source: RuntimeTextSurface,
pub entries: Vec<RuntimeMemoryEntry>,
}
#[derive(Debug, Clone)]
pub struct LoadedRuntimeExecutionGates {
pub source: RuntimeTextSurface,
pub view: session_gates::ExecutionGatesView,
}
#[derive(Debug, Clone)]
pub struct LoadedRuntimeRecoveryState {
pub state: RuntimeRecoveryState,
pub sources: RuntimeRecoverySurfaces,
}
#[derive(Debug, Clone, Serialize)]
pub struct RuntimeRecoveryView {
pub status: &'static str,
pub checkpoint: RuntimeCheckpointView,
pub working_buffer: RuntimeWorkingBufferView,
}
#[derive(Debug, Clone, Serialize)]
pub struct RuntimeCheckpointView {
pub path: String,
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<RecoveryOrigin>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captured_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_started_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub immediate_actions: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub key_files: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RuntimeWorkingBufferView {
pub path: String,
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<RecoveryOrigin>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captured_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_started_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub summary_lines: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct NativeCloneRuntimeState {
#[serde(rename = "schema_version")]
_schema_version: u32,
handoff: RuntimeHandoffState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NativeProfileRuntimeState {
schema_version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
memory_adapter_fingerprint: Option<String>,
#[serde(default)]
memory: Vec<RuntimeMemoryEntry>,
#[serde(default)]
memory_automation: RuntimeMemoryAutomationLedger,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NativeRepoRuntimeState {
schema_version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
repo_memory_adapter_fingerprint: Option<String>,
#[serde(default)]
repo_memory: Vec<RuntimeMemoryEntry>,
#[serde(default)]
memory_automation: RuntimeMemoryAutomationLedger,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct CheckpointFile {
schema_version: u32,
origin: RecoveryOrigin,
captured_at_epoch_s: u64,
session_started_at_epoch_s: u64,
summary: String,
#[serde(default)]
immediate_actions: Vec<String>,
#[serde(default)]
key_files: Vec<String>,
}
pub fn load_runtime_state(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeState> {
load_runtime_state_internal(repo_root, layout, locality_id)
}
pub struct RawRuntimeSources {
pub profile_memory: RuntimeTextSurface,
pub locality_memory: RuntimeTextSurface,
pub pod_memory: RuntimeTextSurface,
pub branch_memory: RuntimeTextSurface,
pub clone_memory: RuntimeTextSurface,
pub execution_gates: LoadedRuntimeExecutionGates,
pub handoff: RuntimeTextSurface,
pub(crate) handoff_state: Option<RuntimeHandoffState>,
pub recovery: LoadedRuntimeRecoveryState,
pub pod_identity_active: bool,
}
pub fn load_raw_start_runtime_sources(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
pod_memory_name: Option<&str>,
pod_identity_active: bool,
) -> Result<RawRuntimeSources> {
let profile_memory = load_profile_memory_surface_internal(layout)?.source;
let locality_memory = load_repo_runtime_surfaces_internal(layout, locality_id)?;
let pod_memory = match pod_memory_name {
Some(name) => load_pod_memory_surface(layout, name)?.source,
None => {
let placeholder = layout
.ccd_root()
.join("pods")
.join("_none")
.join("memory.md");
RuntimeTextSurface::missing("pod_memory", &placeholder)
}
};
let branch_memory = load_branch_memory_surface_internal(repo_root, layout, locality_id)?.source;
let clone_memory = load_clone_memory_surface_internal(layout)?.source;
let execution_gates = load_execution_gates(layout)?;
let (handoff_state, handoff) = load_runtime_handoff_surface(layout)?;
let recovery = load_runtime_recovery_state(layout)?;
Ok(RawRuntimeSources {
profile_memory,
locality_memory: locality_memory.source,
pod_memory,
branch_memory,
clone_memory,
execution_gates,
handoff,
handoff_state: Some(handoff_state),
recovery,
pod_identity_active,
})
}
pub fn normalize_raw_into_loaded(raw: RawRuntimeSources) -> Result<LoadedRuntimeState> {
build_loaded_runtime_state(raw)
}
pub fn load_profile_memory_surface(layout: &StateLayout) -> Result<LoadedRuntimeMemorySurface> {
load_profile_memory_surface_internal(layout)
}
pub fn load_locality_memory_surface(
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeMemorySurface> {
load_locality_memory_surface_internal(layout, locality_id)
}
pub fn load_branch_memory_surface(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeMemorySurface> {
load_branch_memory_surface_internal(repo_root, layout, locality_id)
}
pub fn load_clone_memory_surface(layout: &StateLayout) -> Result<LoadedRuntimeMemorySurface> {
load_clone_memory_surface_internal(layout)
}
pub fn load_pod_memory_surface(
layout: &StateLayout,
pod_name: &str,
) -> Result<LoadedRuntimeMemorySurface> {
let source = read_optional_text("pod_memory", &layout.pod_memory_path(pod_name)?)?;
validate_memory_surface(&source)?;
Ok(LoadedRuntimeMemorySurface {
entries: normalize_memory_scope(&source.content),
source,
})
}
pub fn resolve_active_branch_memory_path(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<Option<PathBuf>> {
let (path, active) = resolve_branch_memory_path(repo_root, layout, locality_id)?;
Ok(active.then_some(path))
}
pub(crate) fn load_repo_memory_automation_record(
layout: &StateLayout,
locality_id: &str,
dedupe_key: &str,
) -> Result<Option<RuntimeMemoryAutomationRecord>> {
match read_native_repo_runtime_state(layout, locality_id)? {
NativeRepoRuntimeLoad::Missing => Ok(None),
NativeRepoRuntimeLoad::Loaded(state) => {
Ok(state.memory_automation.get(dedupe_key).cloned())
}
NativeRepoRuntimeLoad::Invalid(_) => Ok(None),
}
}
pub(crate) fn load_repo_memory_automation_ledger(
layout: &StateLayout,
locality_id: &str,
) -> Result<RuntimeMemoryAutomationLedger> {
match read_native_repo_runtime_state(layout, locality_id)? {
NativeRepoRuntimeLoad::Missing => Ok(RuntimeMemoryAutomationLedger::new()),
NativeRepoRuntimeLoad::Loaded(state) => Ok(state.memory_automation),
NativeRepoRuntimeLoad::Invalid(error) => Err(error),
}
}
pub(crate) fn load_profile_memory_automation_record(
layout: &StateLayout,
dedupe_key: &str,
) -> Result<Option<RuntimeMemoryAutomationRecord>> {
match read_native_profile_runtime_state(layout)? {
NativeProfileRuntimeLoad::Missing => Ok(None),
NativeProfileRuntimeLoad::Loaded(state) => {
Ok(state.memory_automation.get(dedupe_key).cloned())
}
NativeProfileRuntimeLoad::Invalid(_) => Ok(None),
}
}
pub(crate) fn load_profile_memory_automation_ledger(
layout: &StateLayout,
) -> Result<RuntimeMemoryAutomationLedger> {
match read_native_profile_runtime_state(layout)? {
NativeProfileRuntimeLoad::Missing => Ok(RuntimeMemoryAutomationLedger::new()),
NativeProfileRuntimeLoad::Loaded(state) => Ok(state.memory_automation),
NativeProfileRuntimeLoad::Invalid(error) => Err(error),
}
}
pub(crate) fn persist_repo_memory_automation_record(
layout: &StateLayout,
locality_id: &str,
dedupe_key: &str,
record: RuntimeMemoryAutomationRecord,
) -> Result<()> {
persist_native_repo_runtime_state(layout, locality_id, |store| {
store
.memory_automation
.insert(dedupe_key.to_owned(), record);
})
}
pub(crate) fn persist_profile_memory_automation_record(
layout: &StateLayout,
dedupe_key: &str,
record: RuntimeMemoryAutomationRecord,
) -> Result<()> {
persist_native_profile_runtime_state(layout, |store| {
store
.memory_automation
.insert(dedupe_key.to_owned(), record);
})
}
pub fn load_execution_gates(layout: &StateLayout) -> Result<LoadedRuntimeExecutionGates> {
let path = layout.state_db_path();
let Some(db) = try_open_handoff_db(layout)? else {
return Ok(LoadedRuntimeExecutionGates {
source: RuntimeTextSurface::missing("execution_gates", &path),
view: session_gates::build_view(layout, None),
});
};
let raw = db::session_gates::read(db.conn())?;
Ok(LoadedRuntimeExecutionGates {
source: execution_gates_surface(layout, raw.as_ref())?,
view: session_gates::build_view(layout, raw.clone()),
})
}
pub fn load_runtime_recovery_state(layout: &StateLayout) -> Result<LoadedRuntimeRecoveryState> {
let db_path = layout.state_db_path();
let Some(db) = try_open_handoff_db(layout)? else {
return Ok(LoadedRuntimeRecoveryState {
state: RuntimeRecoveryState {
checkpoint: None,
working_buffer: None,
},
sources: RuntimeRecoverySurfaces {
checkpoint: RuntimeRecoverySurface::missing("checkpoint", &db_path),
working_buffer: RuntimeRecoverySurface::missing("working_buffer", &db_path),
},
});
};
let checkpoint = db::recovery::read_checkpoint(db.conn())?;
let working_buffer = db::recovery::read_working_buffer(db.conn())?;
let checkpoint_source = RuntimeRecoverySurface {
kind: "checkpoint",
path: db_path.clone(),
status: if checkpoint.is_some() {
"loaded"
} else {
"missing"
},
};
let working_buffer_source = RuntimeRecoverySurface {
kind: "working_buffer",
path: db_path,
status: if working_buffer.is_some() {
"loaded"
} else {
"missing"
},
};
Ok(LoadedRuntimeRecoveryState {
state: RuntimeRecoveryState {
checkpoint,
working_buffer,
},
sources: RuntimeRecoverySurfaces {
checkpoint: checkpoint_source,
working_buffer: working_buffer_source,
},
})
}
pub fn recovery_view(recovery: &LoadedRuntimeRecoveryState) -> RuntimeRecoveryView {
let checkpoint = checkpoint_view(
&recovery.sources.checkpoint,
recovery.state.checkpoint.as_ref(),
);
let working_buffer = working_buffer_view(
&recovery.sources.working_buffer,
recovery.state.working_buffer.as_ref(),
);
RuntimeRecoveryView {
status: if recovery.state.checkpoint.is_some() || recovery.state.working_buffer.is_some() {
"loaded"
} else {
"missing"
},
checkpoint,
working_buffer,
}
}
fn checkpoint_view(
source: &RuntimeRecoverySurface,
checkpoint: Option<&RuntimeCheckpointState>,
) -> RuntimeCheckpointView {
if let Some(checkpoint) = checkpoint {
return RuntimeCheckpointView {
path: source.path.display().to_string(),
status: source.status,
origin: Some(checkpoint.origin),
captured_at_epoch_s: Some(checkpoint.captured_at_epoch_s),
session_started_at_epoch_s: Some(checkpoint.session_started_at_epoch_s),
summary: Some(checkpoint.summary.clone()),
immediate_actions: checkpoint.immediate_actions.clone(),
key_files: checkpoint.key_files.clone(),
};
}
RuntimeCheckpointView {
path: source.path.display().to_string(),
status: source.status,
origin: None,
captured_at_epoch_s: None,
session_started_at_epoch_s: None,
summary: None,
immediate_actions: Vec::new(),
key_files: Vec::new(),
}
}
fn working_buffer_view(
source: &RuntimeRecoverySurface,
working_buffer: Option<&RuntimeWorkingBufferState>,
) -> RuntimeWorkingBufferView {
if let Some(working_buffer) = working_buffer {
return RuntimeWorkingBufferView {
path: source.path.display().to_string(),
status: source.status,
origin: Some(working_buffer.origin),
captured_at_epoch_s: Some(working_buffer.captured_at_epoch_s),
session_started_at_epoch_s: Some(working_buffer.session_started_at_epoch_s),
summary_lines: working_buffer.summary_lines.clone(),
};
}
RuntimeWorkingBufferView {
path: source.path.display().to_string(),
status: source.status,
origin: None,
captured_at_epoch_s: None,
session_started_at_epoch_s: None,
summary_lines: Vec::new(),
}
}
pub fn native_handoff_exists(layout: &StateLayout) -> Result<bool> {
let Some(db) = try_open_handoff_db(layout)? else {
return Ok(false);
};
db::handoff::exists(db.conn())
}
fn load_runtime_state_internal(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeState> {
let pod_binding = pod_identity::resolve_pod_memory_binding(layout, locality_id)?;
normalize_raw_into_loaded(load_raw_start_runtime_sources(
repo_root,
layout,
locality_id,
pod_binding.as_ref().map(|binding| binding.name.as_str()),
pod_binding
.as_ref()
.and_then(|binding| binding.identity.as_ref())
.is_some(),
)?)
}
fn load_profile_memory_surface_internal(
layout: &StateLayout,
) -> Result<LoadedRuntimeMemorySurface> {
let source = read_optional_text("profile_memory", &layout.profile_memory_path())?;
let native = read_native_profile_runtime_state(layout)?;
load_runtime_memory_surface_from_native(
source,
native_profile_memory_surface(layout, native),
|entries, adapter_contents| {
persist_native_profile_memory_state(layout, entries, adapter_contents)
},
)
}
fn load_locality_memory_surface_internal(
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeMemorySurface> {
let source = read_optional_text(
"locality_memory",
&layout.locality_memory_path(locality_id)?,
)?;
let native = read_native_repo_runtime_state(layout, locality_id)?;
load_runtime_memory_surface_from_native(
source,
native_repo_memory_surface(layout, locality_id, native)?,
|entries, adapter_contents| {
persist_native_repo_memory_state(layout, locality_id, entries, adapter_contents)
},
)
}
fn load_branch_memory_surface_internal(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeMemorySurface> {
let (path, active) = resolve_branch_memory_path(repo_root, layout, locality_id)?;
let source = if active {
read_optional_text("branch_memory", &path)?
} else {
RuntimeTextSurface::missing("branch_memory", &path)
};
validate_memory_surface(&source)?;
Ok(LoadedRuntimeMemorySurface {
entries: normalize_memory_scope(&source.content),
source,
})
}
fn load_clone_memory_surface_internal(layout: &StateLayout) -> Result<LoadedRuntimeMemorySurface> {
let source = read_optional_text("clone_memory", &layout.clone_memory_path())?;
validate_memory_surface(&source)?;
Ok(LoadedRuntimeMemorySurface {
entries: normalize_memory_scope(&source.content),
source,
})
}
fn resolve_branch_memory_path(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
) -> Result<(PathBuf, bool)> {
let git = match handoff::read_git_state(repo_root, handoff::BranchMode::AllowDetachedHead) {
Ok(git) => git,
Err(_) => return Ok((layout.branch_memory_path(locality_id, "HEAD")?, false)),
};
let path = layout.branch_memory_path(locality_id, &git.branch)?;
let active = git.branch != "HEAD"
&& !handoff::is_trunk_branch(&git.branch)
&& handoff::branch_merged_into_trunk(repo_root, &git.branch).is_none();
Ok((path, active))
}
fn load_repo_runtime_surfaces_internal(
layout: &StateLayout,
locality_id: &str,
) -> Result<LoadedRuntimeMemorySurface> {
let locality_memory_source = read_optional_text(
"locality_memory",
&layout.locality_memory_path(locality_id)?,
)?;
let native = read_native_repo_runtime_state(layout, locality_id)?;
let native_memory = match native {
NativeRepoRuntimeLoad::Missing => NativeMemorySurfaceLoad::Missing,
NativeRepoRuntimeLoad::Loaded(state) => {
native_repo_memory_from_state(layout.locality_runtime_state_path(locality_id)?, state)
}
NativeRepoRuntimeLoad::Invalid(error) => {
if locality_memory_source.is_missing() {
return Err(error);
}
NativeMemorySurfaceLoad::Missing
}
};
let locality_memory = load_runtime_memory_surface_from_native(
locality_memory_source,
native_memory,
|entries, adapter_contents| {
persist_native_repo_memory_state(layout, locality_id, entries, adapter_contents)
},
)?;
Ok(locality_memory)
}
fn load_runtime_memory_surface_from_native(
source: RuntimeTextSurface,
native: NativeMemorySurfaceLoad,
persist_native: impl Fn(&[RuntimeMemoryEntry], Option<&str>) -> Result<()>,
) -> Result<LoadedRuntimeMemorySurface> {
if !source.is_missing() {
validate_memory_surface(&source)?;
let entries = normalize_memory_scope(&source.content);
let adapter_fingerprint = content_fingerprint(&source.content);
let native_current = matches!(
&native,
NativeMemorySurfaceLoad::Loaded(state)
if state.adapter_fingerprint.as_deref() == Some(adapter_fingerprint.as_str())
);
if !native_current {
persist_native(&entries, Some(&source.content))?;
}
return Ok(LoadedRuntimeMemorySurface { source, entries });
}
match native {
NativeMemorySurfaceLoad::Loaded(state) => Ok(LoadedRuntimeMemorySurface {
source: state.source,
entries: state.entries,
}),
NativeMemorySurfaceLoad::Missing => Ok(LoadedRuntimeMemorySurface {
source,
entries: Vec::new(),
}),
NativeMemorySurfaceLoad::Invalid(error) => Err(error),
}
}
pub fn structured_memory_view(
sources: &RuntimeSourceSurfaces,
) -> memory_entries::StructuredMemoryView {
memory_entries::inspect_sources_with_branch_and_clone(
some_if_present(&sources.profile_memory),
some_if_present(&sources.locality_memory),
some_if_present(&sources.pod_memory),
some_if_present(&sources.branch_memory),
some_if_present(&sources.clone_memory),
)
}
pub(crate) fn memory_state_from_sources(sources: &RuntimeSourceSurfaces) -> RuntimeMemoryState {
RuntimeMemoryState {
profile: normalize_memory_scope(&sources.profile_memory.content),
locality: normalize_memory_scope(&sources.locality_memory.content),
pod: normalize_memory_scope(&sources.pod_memory.content),
branch: normalize_memory_scope(&sources.branch_memory.content),
clone: normalize_memory_scope(&sources.clone_memory.content),
}
}
fn open_handoff_db(layout: &StateLayout) -> Result<db::StateDb> {
let db = db::StateDb::open(&layout.state_db_path())?;
migrate_handoff_json(&db, layout)?;
Ok(db)
}
fn try_open_handoff_db(layout: &StateLayout) -> Result<Option<db::StateDb>> {
db::StateDb::try_open_with_migration(
layout,
|| {
layout.clone_runtime_state_path().exists()
|| layout.clone_checkpoint_path().exists()
|| layout.clone_working_buffer_path().exists()
},
|db| migrate_handoff_json(db, layout),
)
}
fn migrate_handoff_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
migrate_handoff_state_json(db, layout)?;
migrate_checkpoint_json(db, layout)?;
migrate_working_buffer_md(db, layout)?;
Ok(())
}
fn migrate_handoff_state_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
let path = layout.clone_runtime_state_path();
let contents = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
};
if db::handoff::read(db.conn())?.is_none() {
let native: NativeCloneRuntimeState = serde_json::from_str(&contents)
.with_context(|| format!("failed to parse {}", path.display()))?;
db::handoff::write(db.conn(), &native.handoff)?;
}
let mut migrated = path.as_os_str().to_owned();
migrated.push(".migrated");
fs::rename(&path, Path::new(&migrated))
.with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
Ok(())
}
fn migrate_checkpoint_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
let path = layout.clone_checkpoint_path();
let contents = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
};
if !contents.trim().is_empty() && db::recovery::read_checkpoint(db.conn())?.is_none() {
let checkpoint: CheckpointFile = serde_json::from_str(&contents)
.with_context(|| format!("failed to parse {}", path.display()))?;
if checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION {
bail!(
"{} has unsupported checkpoint schema version {}; expected {}",
path.display(),
checkpoint.schema_version,
CHECKPOINT_SCHEMA_VERSION
);
}
validate_checkpoint(&checkpoint, &path)?;
db::recovery::write_checkpoint(
db.conn(),
&RuntimeCheckpointState {
origin: checkpoint.origin,
captured_at_epoch_s: checkpoint.captured_at_epoch_s,
session_started_at_epoch_s: checkpoint.session_started_at_epoch_s,
summary: checkpoint.summary,
immediate_actions: checkpoint.immediate_actions,
key_files: checkpoint.key_files,
},
)?;
}
let mut migrated = path.as_os_str().to_owned();
migrated.push(".migrated");
fs::rename(&path, Path::new(&migrated))
.with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
Ok(())
}
fn migrate_working_buffer_md(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
let path = layout.clone_working_buffer_path();
let contents = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
};
if !contents.trim().is_empty() && db::recovery::read_working_buffer(db.conn())?.is_none() {
if contents.len() > MAX_WORKING_BUFFER_BYTES {
bail!(
"{} exceeds the working-buffer size limit of {} bytes",
path.display(),
MAX_WORKING_BUFFER_BYTES
);
}
let provenance = parse_recovery_provenance(
&handoff::extract_bulleted_section(&contents, "Provenance"),
&path,
)?;
let summary_lines = handoff::extract_bulleted_section(&contents, "Recent Exchange Summary");
if summary_lines.is_empty() {
bail!(
"{} is missing `## Recent Exchange Summary` bullet items",
path.display()
);
}
if summary_lines.len() > MAX_WORKING_BUFFER_SUMMARY_LINES {
bail!(
"{} has {} working-buffer summary lines; the limit is {}",
path.display(),
summary_lines.len(),
MAX_WORKING_BUFFER_SUMMARY_LINES
);
}
db::recovery::write_working_buffer(
db.conn(),
&RuntimeWorkingBufferState {
origin: provenance.origin,
captured_at_epoch_s: provenance.captured_at_epoch_s,
session_started_at_epoch_s: provenance.session_started_at_epoch_s,
summary_lines,
},
)?;
}
let mut migrated = path.as_os_str().to_owned();
migrated.push(".migrated");
fs::rename(&path, Path::new(&migrated))
.with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
Ok(())
}
pub fn persist_canonical_handoff_state(
layout: &StateLayout,
handoff: &RuntimeHandoffState,
) -> Result<u64> {
let db = open_handoff_db(layout)?;
db::handoff::write(db.conn(), handoff)
}
pub fn load_canonical_handoff_surface(layout: &StateLayout) -> Result<RuntimeTextSurface> {
let Some(db) = try_open_handoff_db(layout)? else {
return Ok(RuntimeTextSurface::missing(
"handoff",
&layout.state_db_path(),
));
};
match db::handoff::read(db.conn())? {
Some(handoff) => Ok(native_handoff_surface(layout, &handoff)),
None => Ok(RuntimeTextSurface::missing(
"handoff",
&layout.state_db_path(),
)),
}
}
fn build_loaded_runtime_state(raw: RawRuntimeSources) -> Result<LoadedRuntimeState> {
let RawRuntimeSources {
profile_memory,
locality_memory,
pod_memory,
branch_memory,
clone_memory,
execution_gates,
handoff,
handoff_state,
recovery,
pod_identity_active,
} = raw;
validate_memory_surface(&profile_memory)?;
validate_memory_surface(&locality_memory)?;
validate_memory_surface(&pod_memory)?;
validate_memory_surface(&branch_memory)?;
validate_memory_surface(&clone_memory)?;
let state = RuntimeState {
memory: RuntimeMemoryState {
profile: normalize_memory_scope(&profile_memory.content),
locality: normalize_memory_scope(&locality_memory.content),
pod: normalize_memory_scope(&pod_memory.content),
branch: normalize_memory_scope(&branch_memory.content),
clone: normalize_memory_scope(&clone_memory.content),
},
handoff: handoff_state.unwrap_or_else(|| normalize_handoff(&handoff.content)),
};
Ok(LoadedRuntimeState {
state,
sources: RuntimeSourceSurfaces {
profile_memory,
locality_memory,
pod_memory,
branch_memory,
clone_memory,
execution_gates: execution_gates.source.clone(),
handoff,
},
execution_gates,
recovery,
pod_identity_active,
})
}
fn read_optional_text(kind: &'static str, path: &Path) -> Result<RuntimeTextSurface> {
match fs::read_to_string(path) {
Ok(content) => Ok(RuntimeTextSurface {
kind,
path: path.to_path_buf(),
status: if content.is_empty() {
RuntimeTextSurfaceStatus::Empty
} else {
RuntimeTextSurfaceStatus::Loaded
},
content,
migrated_from: None,
}),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Ok(RuntimeTextSurface::missing(kind, path))
}
Err(error) => Err(error).with_context(|| format!("failed to read {}", path.display())),
}
}
fn load_runtime_handoff_surface(
layout: &StateLayout,
) -> Result<(RuntimeHandoffState, RuntimeTextSurface)> {
let Some(db) = try_open_handoff_db(layout)? else {
return Ok((
RuntimeHandoffState::default(),
RuntimeTextSurface::missing("handoff", &layout.state_db_path()),
));
};
match db::handoff::read(db.conn())? {
Some(handoff) => {
let surface = native_handoff_surface(layout, &handoff);
Ok((handoff, surface))
}
None => Ok((
RuntimeHandoffState::default(),
RuntimeTextSurface::missing("handoff", &layout.state_db_path()),
)),
}
}
fn execution_gates_surface(
layout: &StateLayout,
raw: Option<&session_gates::ExecutionGateStateFile>,
) -> Result<RuntimeTextSurface> {
let Some(raw) = raw else {
return Ok(RuntimeTextSurface::missing(
"execution_gates",
&layout.state_db_path(),
));
};
Ok(RuntimeTextSurface {
kind: "execution_gates",
path: layout.state_db_path(),
status: RuntimeTextSurfaceStatus::LoadedNative,
content: serde_json::to_string_pretty(raw)?,
migrated_from: None,
})
}
fn native_handoff_surface(
layout: &StateLayout,
handoff: &RuntimeHandoffState,
) -> RuntimeTextSurface {
RuntimeTextSurface {
kind: "handoff",
path: layout.state_db_path(),
status: RuntimeTextSurfaceStatus::LoadedNative,
content: render_handoff_markdown(handoff),
migrated_from: None,
}
}
fn native_profile_memory_surface(
layout: &StateLayout,
native: NativeProfileRuntimeLoad,
) -> NativeMemorySurfaceLoad {
match native {
NativeProfileRuntimeLoad::Missing => NativeMemorySurfaceLoad::Missing,
NativeProfileRuntimeLoad::Invalid(error) => NativeMemorySurfaceLoad::Invalid(error),
NativeProfileRuntimeLoad::Loaded(state) => {
NativeMemorySurfaceLoad::Loaded(NativeMemorySurfaceState {
source: RuntimeTextSurface {
kind: "profile_memory",
path: layout.profile_runtime_state_path(),
status: RuntimeTextSurfaceStatus::LoadedNative,
content: render_memory_markdown("Profile Memory", &state.memory),
migrated_from: None,
},
adapter_fingerprint: state.memory_adapter_fingerprint,
entries: state.memory,
})
}
}
}
fn native_repo_memory_surface(
layout: &StateLayout,
locality_id: &str,
native: NativeRepoRuntimeLoad,
) -> Result<NativeMemorySurfaceLoad> {
match native {
NativeRepoRuntimeLoad::Missing => Ok(NativeMemorySurfaceLoad::Missing),
NativeRepoRuntimeLoad::Invalid(error) => Ok(NativeMemorySurfaceLoad::Invalid(error)),
NativeRepoRuntimeLoad::Loaded(state) => Ok(native_repo_memory_from_state(
layout.locality_runtime_state_path(locality_id)?,
state,
)),
}
}
fn native_repo_memory_from_state(
path: PathBuf,
state: NativeRepoRuntimeState,
) -> NativeMemorySurfaceLoad {
NativeMemorySurfaceLoad::Loaded(NativeMemorySurfaceState {
source: RuntimeTextSurface {
kind: "locality_memory",
path,
status: RuntimeTextSurfaceStatus::LoadedNative,
content: render_memory_markdown("Project Memory", &state.repo_memory),
migrated_from: None,
},
adapter_fingerprint: state.repo_memory_adapter_fingerprint,
entries: state.repo_memory,
})
}
fn read_native_profile_runtime_state(layout: &StateLayout) -> Result<NativeProfileRuntimeLoad> {
let path = layout.profile_runtime_state_path();
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(NativeProfileRuntimeLoad::Missing)
}
Err(error) => {
return Err(error).with_context(|| format!("failed to read {}", path.display()))
}
};
let parsed = serde_json::from_str::<NativeProfileRuntimeState>(&contents)
.with_context(|| format!("failed to parse {}", path.display()));
match parsed {
Ok(state) if state.schema_version == NATIVE_PROFILE_RUNTIME_SCHEMA_VERSION => {
Ok(NativeProfileRuntimeLoad::Loaded(state))
}
Ok(_) => Ok(NativeProfileRuntimeLoad::Invalid(anyhow!(
"{} has unsupported schema version",
path.display()
))),
Err(error) => Ok(NativeProfileRuntimeLoad::Invalid(error)),
}
}
fn read_native_repo_runtime_state(
layout: &StateLayout,
locality_id: &str,
) -> Result<NativeRepoRuntimeLoad> {
let path = layout.repo_runtime_state_path(locality_id)?;
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(NativeRepoRuntimeLoad::Missing)
}
Err(error) => {
return Err(error).with_context(|| format!("failed to read {}", path.display()))
}
};
if contents.trim().is_empty() {
return Ok(NativeRepoRuntimeLoad::Missing);
}
let parsed = serde_json::from_str::<NativeRepoRuntimeState>(&contents)
.with_context(|| format!("failed to parse {}", path.display()));
match parsed {
Ok(state) if state.schema_version == NATIVE_REPO_RUNTIME_SCHEMA_VERSION => {
Ok(NativeRepoRuntimeLoad::Loaded(state))
}
Ok(_) => Ok(NativeRepoRuntimeLoad::Invalid(anyhow!(
"{} has unsupported schema version",
path.display()
))),
Err(error) => Ok(NativeRepoRuntimeLoad::Invalid(error)),
}
}
fn persist_native_profile_memory_state(
layout: &StateLayout,
memory: &[RuntimeMemoryEntry],
adapter_contents: Option<&str>,
) -> Result<()> {
persist_native_profile_runtime_state(layout, |store| {
store.memory_adapter_fingerprint = adapter_contents.map(content_fingerprint);
store.memory = memory.to_vec();
})
}
fn persist_native_repo_memory_state(
layout: &StateLayout,
locality_id: &str,
memory: &[RuntimeMemoryEntry],
adapter_contents: Option<&str>,
) -> Result<()> {
persist_native_repo_runtime_state(layout, locality_id, |store| {
store.repo_memory_adapter_fingerprint = adapter_contents.map(content_fingerprint);
store.repo_memory = memory.to_vec();
})
}
fn empty_native_repo_runtime_state() -> NativeRepoRuntimeState {
NativeRepoRuntimeState {
schema_version: NATIVE_REPO_RUNTIME_SCHEMA_VERSION,
repo_memory_adapter_fingerprint: None,
repo_memory: Vec::new(),
memory_automation: BTreeMap::new(),
}
}
fn empty_native_profile_runtime_state() -> NativeProfileRuntimeState {
NativeProfileRuntimeState {
schema_version: NATIVE_PROFILE_RUNTIME_SCHEMA_VERSION,
memory_adapter_fingerprint: None,
memory: Vec::new(),
memory_automation: BTreeMap::new(),
}
}
fn persist_native_profile_runtime_state(
layout: &StateLayout,
mutate: impl FnOnce(&mut NativeProfileRuntimeState),
) -> Result<()> {
let mut store = match read_native_profile_runtime_state(layout)? {
NativeProfileRuntimeLoad::Loaded(store) => store,
NativeProfileRuntimeLoad::Missing | NativeProfileRuntimeLoad::Invalid(_) => {
empty_native_profile_runtime_state()
}
};
mutate(&mut store);
store.schema_version = NATIVE_PROFILE_RUNTIME_SCHEMA_VERSION;
let path = layout.profile_runtime_state_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
path_write::replace_text(&path, &serde_json::to_string_pretty(&store)?, None)
.with_context(|| format!("failed to write {}", path.display()))
}
fn persist_native_repo_runtime_state(
layout: &StateLayout,
locality_id: &str,
mutate: impl FnOnce(&mut NativeRepoRuntimeState),
) -> Result<()> {
let mut store = match read_native_repo_runtime_state(layout, locality_id)? {
NativeRepoRuntimeLoad::Loaded(store) => store,
NativeRepoRuntimeLoad::Missing | NativeRepoRuntimeLoad::Invalid(_) => {
empty_native_repo_runtime_state()
}
};
store.schema_version = NATIVE_REPO_RUNTIME_SCHEMA_VERSION;
mutate(&mut store);
let path = layout.repo_runtime_state_path(locality_id)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
path_write::replace_text(&path, &serde_json::to_string_pretty(&store)?, None)
.with_context(|| format!("failed to write {}", path.display()))
}
fn content_fingerprint(contents: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(contents.as_bytes());
format!("{:x}", hasher.finalize())
}
struct RecoveryProvenance {
origin: RecoveryOrigin,
captured_at_epoch_s: u64,
session_started_at_epoch_s: u64,
}
fn parse_recovery_provenance(items: &[String], path: &Path) -> Result<RecoveryProvenance> {
let mut values = BTreeMap::new();
for item in items {
let (key, value) = item.split_once(':').ok_or_else(|| {
anyhow!(
"{} has an invalid recovery provenance item `{item}`; expected `Key: value`",
path.display()
)
})?;
values.insert(key.trim().to_ascii_lowercase(), value.trim().to_owned());
}
let origin = parse_recovery_origin(
values
.remove("origin")
.ok_or_else(|| anyhow!("{} is missing `Origin` in `## Provenance`", path.display()))?,
path,
)?;
let captured_at_epoch_s = parse_required_epoch(
values.remove("captured at epoch").ok_or_else(|| {
anyhow!(
"{} is missing `Captured At Epoch` in `## Provenance`",
path.display()
)
})?,
path,
"Captured At Epoch",
)?;
let session_started_at_epoch_s = parse_required_epoch(
values.remove("session started at epoch").ok_or_else(|| {
anyhow!(
"{} is missing `Session Started At Epoch` in `## Provenance`",
path.display()
)
})?,
path,
"Session Started At Epoch",
)?;
if !values.is_empty() {
let unexpected = values.keys().cloned().collect::<Vec<_>>().join(", ");
bail!(
"{} has unsupported recovery provenance keys: {}",
path.display(),
unexpected
);
}
Ok(RecoveryProvenance {
origin,
captured_at_epoch_s,
session_started_at_epoch_s,
})
}
fn parse_recovery_origin(value: String, path: &Path) -> Result<RecoveryOrigin> {
match value.as_str() {
"compaction" => Ok(RecoveryOrigin::Compaction),
"risky_pause" => Ok(RecoveryOrigin::RiskyPause),
"manual" => Ok(RecoveryOrigin::Manual),
_ => bail!(
"{} has unsupported recovery origin `{value}`; expected `compaction`, `risky_pause`, or `manual`",
path.display()
),
}
}
fn parse_required_epoch(value: String, path: &Path, label: &str) -> Result<u64> {
let parsed = value.parse::<u64>().with_context(|| {
format!(
"{} has an invalid `{label}` value `{value}`; expected an unsigned epoch second",
path.display()
)
})?;
if parsed == 0 {
bail!(
"{} has invalid `{label}` value `0`; expected a positive epoch second",
path.display()
);
}
Ok(parsed)
}
fn validate_checkpoint(checkpoint: &CheckpointFile, path: &Path) -> Result<()> {
if checkpoint.captured_at_epoch_s == 0 {
bail!(
"{} has invalid `captured_at_epoch_s` value `0`; expected a positive epoch second",
path.display()
);
}
if checkpoint.session_started_at_epoch_s == 0 {
bail!(
"{} has invalid `session_started_at_epoch_s` value `0`; expected a positive epoch second",
path.display()
);
}
let summary = checkpoint.summary.trim();
if summary.is_empty() {
bail!("{} has an empty checkpoint summary", path.display());
}
if summary.len() > MAX_CHECKPOINT_SUMMARY_CHARS {
bail!(
"{} has a checkpoint summary longer than {} characters",
path.display(),
MAX_CHECKPOINT_SUMMARY_CHARS
);
}
validate_recovery_items(
"immediate_actions",
&checkpoint.immediate_actions,
MAX_RECOVERY_IMMEDIATE_ACTIONS,
path,
)?;
validate_recovery_items(
"key_files",
&checkpoint.key_files,
MAX_RECOVERY_KEY_FILES,
path,
)?;
Ok(())
}
fn validate_recovery_items(
label: &str,
items: &[String],
max_items: usize,
path: &Path,
) -> Result<()> {
if items.len() > max_items {
bail!(
"{} has {} `{label}` entries; the limit is {}",
path.display(),
items.len(),
max_items
);
}
if items.iter().any(|item| item.trim().is_empty()) {
bail!(
"{} has an empty `{label}` entry; recovery artifacts must use explicit non-empty items",
path.display()
);
}
Ok(())
}
fn validate_memory_surface(surface: &RuntimeTextSurface) -> Result<()> {
let report = memory_entries::parse_document(&surface.content);
if report.diagnostics.is_empty() {
return Ok(());
}
Err(anyhow!(
"{} has invalid structured CCD memory entries: {}",
surface.path.display(),
report.diagnostics.join("; ")
))
}
fn some_if_present(surface: &RuntimeTextSurface) -> Option<&str> {
if surface.is_missing() {
None
} else {
Some(&surface.content)
}
}
pub(crate) fn render_handoff_markdown(state: &RuntimeHandoffState) -> String {
let mut body = String::new();
if !state.title.is_empty() {
body.push_str("# ");
body.push_str(&state.title);
body.push_str("\n\n");
}
append_numbered_section(&mut body, "Immediate Actions", &state.immediate_actions);
append_bulleted_section(&mut body, "Completed State", &state.completed_state);
append_bulleted_section(
&mut body,
"Operational Guardrails",
&state.operational_guardrails,
);
append_bulleted_section(
&mut body,
handoff::KEY_FILES_SECTION_TITLE,
&state.key_files,
);
append_numbered_section(&mut body, "Definition of Done", &state.definition_of_done);
body
}
fn append_bulleted_section(body: &mut String, title: &str, items: &[RuntimeHandoffItem]) {
let active_items = items
.iter()
.filter(|item| item.lifecycle.is_active())
.collect::<Vec<_>>();
if active_items.is_empty() {
return;
}
if !body.is_empty() {
body.push('\n');
}
body.push_str("## ");
body.push_str(title);
body.push_str("\n\n");
for item in active_items {
body.push_str("- ");
body.push_str(&item.text);
body.push('\n');
}
}
fn append_numbered_section(body: &mut String, title: &str, items: &[RuntimeHandoffItem]) {
let active_items = items
.iter()
.filter(|item| item.lifecycle.is_active())
.collect::<Vec<_>>();
if active_items.is_empty() {
return;
}
if !body.is_empty() {
body.push('\n');
}
body.push_str("## ");
body.push_str(title);
body.push_str("\n\n");
for (i, item) in active_items.iter().enumerate() {
body.push_str(&format!("{}. {}\n", i + 1, item.text));
}
}
enum NativeProfileRuntimeLoad {
Missing,
Loaded(NativeProfileRuntimeState),
Invalid(anyhow::Error),
}
enum NativeRepoRuntimeLoad {
Missing,
Loaded(NativeRepoRuntimeState),
Invalid(anyhow::Error),
}
struct NativeMemorySurfaceState {
source: RuntimeTextSurface,
adapter_fingerprint: Option<String>,
entries: Vec<RuntimeMemoryEntry>,
}
enum NativeMemorySurfaceLoad {
Missing,
Loaded(NativeMemorySurfaceState),
Invalid(anyhow::Error),
}
fn render_memory_markdown(title: &str, entries: &[RuntimeMemoryEntry]) -> String {
if entries.is_empty() {
return String::new();
}
let mut body = format!("# {title}\n");
let narrative = entries
.iter()
.filter(|entry| matches!(&entry.origin, RuntimeMemoryOrigin::Narrative))
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>();
if !narrative.is_empty() {
body.push_str("\n## Active Notes\n\n");
for item in &narrative {
body.push_str("- ");
body.push_str(item);
body.push('\n');
}
}
let structured = entries
.iter()
.filter_map(RuntimeMemoryEntry::as_structured_entry)
.collect::<Vec<_>>();
if !structured.is_empty() {
if !body.ends_with("\n\n") {
body.push('\n');
}
for (index, entry) in structured.iter().enumerate() {
if index > 0 {
body.push('\n');
}
body.push_str(&memory_file::render_entry_block(entry));
body.push('\n');
}
}
body
}
fn normalize_memory_scope(contents: &str) -> Vec<RuntimeMemoryEntry> {
let report = memory_entries::parse_document(contents);
let mut items = extract_narrative_memory_lines(&strip_ccd_memory_blocks(contents))
.into_iter()
.map(RuntimeMemoryEntry::narrative)
.collect::<Vec<_>>();
if report.diagnostics.is_empty() {
items.extend(
report
.entries
.into_iter()
.map(RuntimeMemoryEntry::from_structured_entry),
);
}
items
}
fn runtime_memory_lifecycle(state: &str) -> RuntimeLifecycle {
if state == "active" {
RuntimeLifecycle::Active
} else {
RuntimeLifecycle::Inactive
}
}
pub(crate) fn normalize_handoff(contents: &str) -> RuntimeHandoffState {
if contents.is_empty() {
return RuntimeHandoffState::default();
}
let title = contents
.lines()
.find_map(|line| line.strip_prefix("# ").map(str::trim))
.unwrap_or_default()
.to_owned();
RuntimeHandoffState {
title,
immediate_actions: handoff::extract_numbered_section(contents, "Immediate Actions")
.into_iter()
.map(active_handoff_item)
.collect(),
completed_state: handoff::extract_bulleted_section(contents, "Completed State")
.into_iter()
.filter(|line| !starts_with_inactive_marker(line))
.map(active_handoff_item)
.collect(),
operational_guardrails: handoff::extract_bulleted_section(
contents,
"Operational Guardrails",
)
.into_iter()
.map(active_handoff_item)
.collect(),
key_files: handoff::extract_bulleted_section(contents, handoff::KEY_FILES_SECTION_TITLE)
.into_iter()
.map(active_handoff_item)
.collect(),
definition_of_done: handoff::extract_numbered_section(contents, "Definition of Done")
.into_iter()
.map(active_handoff_item)
.collect(),
}
}
fn active_handoff_item(text: String) -> RuntimeHandoffItem {
RuntimeHandoffItem {
text,
lifecycle: RuntimeLifecycle::Active,
}
}
fn extract_narrative_memory_lines(contents: &str) -> Vec<String> {
extract_generic_markdown_lines(
contents,
MarkdownLineExtraction {
inactive_headings: &[
"archived",
"archive",
"historical",
"history",
"superseded",
"stale",
],
inactive_item_prefixes: &["archived:", "stale:", "superseded:"],
inline_skip_keywords: &["archived:", "stale:"],
},
)
}
fn strip_ccd_memory_blocks(contents: &str) -> String {
strip_fenced_blocks(contents, CCD_MEMORY_OPENING_FENCE, GENERIC_CLOSING_FENCE)
}
fn starts_with_inactive_marker(line: &str) -> bool {
starts_with_any_prefix_case_insensitive(line, &["archived:", "stale:", "superseded:"])
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use serde_json::Value as JsonValue;
use tempfile::tempdir;
use super::*;
use crate::profile::ProfileName;
#[test]
fn loads_runtime_state_from_current_authored_memory_and_focus_surfaces() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
let locality_id = "ccdrepo_runtime";
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
.expect("repo overlay dir");
fs::create_dir_all(layout.clone_profile_root()).expect("clone profile root");
fs::write(
layout.profile_memory_path(),
concat!(
"# Profile Memory\n\n",
"- Prefer bounded projections.\n\n",
"```ccd-memory\n",
"id = \"mem_1\"\n",
"type = \"rule\"\n",
"state = \"active\"\n",
"created_at = \"2026-03-10T10:00:00Z\"\n",
"last_touched_session = 1\n",
"origin = \"manual\"\n",
"content = \"Keep cache artifacts disposable.\"\n",
"```\n"
),
)
.expect("profile memory");
fs::write(
layout
.locality_memory_path(locality_id)
.expect("locality memory path"),
"# Repo Memory\n\n- Keep start deterministic.\n",
)
.expect("repo memory");
fs::write(
layout.handoff_path(),
concat!(
"# Next Session: Runtime state\n\n",
"## Immediate Actions\n\n",
"1. Add runtime-state scaffolding.\n\n",
"## Current System State\n\n",
"- Working tree is dirty.\n"
),
)
.expect("handoff");
let loaded =
load_runtime_state(temp.path(), &layout, locality_id).expect("load runtime state");
assert_eq!(
loaded.sources.profile_memory.status,
RuntimeTextSurfaceStatus::Loaded
);
assert_eq!(
loaded
.state
.memory
.profile
.iter()
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>(),
vec![
"Prefer bounded projections.".to_owned(),
"rule: Keep cache artifacts disposable.".to_owned(),
]
);
assert_eq!(
loaded.sources.handoff.status,
RuntimeTextSurfaceStatus::Missing
);
assert!(loaded.state.handoff.title.is_empty());
assert!(loaded.state.handoff.immediate_actions.is_empty());
}
#[test]
fn runtime_memory_surface_preserves_structured_entry_metadata() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::write(
layout.profile_memory_path(),
concat!(
"# Profile Memory\n\n",
"```ccd-memory\n",
"id = \"mem_profile_rule\"\n",
"type = \"rule\"\n",
"state = \"promotion_candidate\"\n",
"created_at = \"2026-03-10T10:00:00Z\"\n",
"last_touched_session = 7\n",
"origin = \"manual\"\n",
"decay_class = \"stable\"\n",
"expires_at = \"2026-06-01T00:00:00Z\"\n",
"tags = [\"rust\", \"lint\"]\n",
"source_ref = \"docs/spec.md#L1\"\n",
"supersedes = [\"mem_old\"]\n",
"content = \"Keep runtime mutations deterministic.\"\n",
"```\n",
),
)
.expect("profile memory");
let loaded = load_profile_memory_surface(&layout).expect("load profile memory");
let entry = loaded
.entries
.iter()
.find(|entry| entry.structured_id() == Some("mem_profile_rule"))
.expect("structured memory entry");
let round_trip = entry
.as_structured_entry()
.expect("round-trip structured entry");
assert_eq!(loaded.source.status, RuntimeTextSurfaceStatus::Loaded);
assert_eq!(entry.structured_state(), Some("promotion_candidate"));
assert_eq!(round_trip.id, "mem_profile_rule");
assert_eq!(round_trip.state, "promotion_candidate");
assert_eq!(round_trip.last_touched_session, 7);
assert_eq!(round_trip.decay_class.as_deref(), Some("stable"));
assert_eq!(
round_trip.expires_at.as_deref(),
Some("2026-06-01T00:00:00Z")
);
assert_eq!(round_trip.tags, vec!["rust".to_owned(), "lint".to_owned()]);
assert_eq!(round_trip.source_ref.as_deref(), Some("docs/spec.md#L1"));
assert_eq!(round_trip.supersedes, vec!["mem_old".to_owned()]);
}
#[test]
fn runtime_memory_surface_preserves_superseded_at() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::write(
layout.profile_memory_path(),
concat!(
"# Profile Memory\n\n",
"```ccd-memory\n",
"id = \"mem_profile_rule\"\n",
"type = \"rule\"\n",
"state = \"superseded\"\n",
"created_at = \"2026-03-10T10:00:00Z\"\n",
"last_touched_session = 7\n",
"origin = \"manual\"\n",
"superseded_at = \"2026-03-12T09:30:00Z\"\n",
"content = \"Keep runtime mutations deterministic.\"\n",
"```\n",
),
)
.expect("profile memory");
let loaded = load_profile_memory_surface(&layout).expect("load profile memory");
let entry = loaded
.entries
.iter()
.find(|entry| entry.structured_id() == Some("mem_profile_rule"))
.expect("structured memory entry");
let round_trip = entry
.as_structured_entry()
.expect("round-trip structured entry");
assert_eq!(entry.structured_state(), Some("superseded"));
assert_eq!(entry.superseded_at.as_deref(), Some("2026-03-12T09:30:00Z"));
assert_eq!(
round_trip.superseded_at.as_deref(),
Some("2026-03-12T09:30:00Z")
);
}
#[test]
fn profile_memory_surface_uses_native_state_when_markdown_is_missing() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::write(
layout.profile_memory_path(),
concat!(
"# Profile Memory\n\n",
"- Prefer native runtime state.\n\n",
"```ccd-memory\n",
"id = \"mem_profile_rule\"\n",
"type = \"rule\"\n",
"state = \"active\"\n",
"created_at = \"2026-03-10T10:00:00Z\"\n",
"last_touched_session = 7\n",
"origin = \"manual\"\n",
"content = \"Keep generated state JSON-native.\"\n",
"```\n",
),
)
.expect("profile memory");
load_profile_memory_surface(&layout).expect("bootstrap native profile memory");
fs::remove_file(layout.profile_memory_path()).expect("remove profile memory markdown");
let loaded = load_profile_memory_surface(&layout).expect("load native profile memory");
let native: JsonValue = serde_json::from_str(
&fs::read_to_string(layout.profile_runtime_state_path()).expect("native profile store"),
)
.expect("parse native profile runtime");
assert_eq!(loaded.source.status, RuntimeTextSurfaceStatus::LoadedNative);
assert_eq!(loaded.source.path, layout.profile_runtime_state_path());
assert_eq!(
loaded
.entries
.iter()
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>(),
vec![
"Prefer native runtime state.".to_owned(),
"rule: Keep generated state JSON-native.".to_owned(),
]
);
assert_eq!(
native["memory"][1]["text"],
"Keep generated state JSON-native."
);
}
#[test]
fn runtime_load_uses_native_repo_memory_when_markdown_is_missing() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
let locality_id = "ccdrepo_runtime";
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
.expect("repo overlay dir");
fs::write(
layout
.locality_memory_path(locality_id)
.expect("locality memory path"),
"# Repo Memory\n\n- Keep repo state JSON-backed.\n",
)
.expect("repo memory");
load_runtime_state(temp.path(), &layout, locality_id)
.expect("bootstrap native repo runtime");
fs::remove_file(
layout
.locality_memory_path(locality_id)
.expect("locality memory path"),
)
.expect("remove repo memory markdown");
let loaded = load_runtime_state(temp.path(), &layout, locality_id)
.expect("load runtime from native repo");
assert_eq!(
loaded.sources.locality_memory.status,
RuntimeTextSurfaceStatus::LoadedNative
);
assert_eq!(
loaded
.state
.memory
.locality
.iter()
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>(),
vec!["Keep repo state JSON-backed.".to_owned()]
);
}
#[test]
fn runtime_load_accepts_legacy_native_repo_state_without_memory_automation() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
let locality_id = "ccdrepo_legacy_runtime";
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
.expect("repo overlay dir");
fs::write(
layout
.repo_runtime_state_path(locality_id)
.expect("repo runtime path"),
serde_json::to_vec_pretty(&serde_json::json!({
"schema_version": 1,
"repo_memory_adapter_fingerprint": null,
"repo_memory": [
{
"id": "mem_repo_rule",
"text": "Legacy runtime JSON still loads.",
"lifecycle": "active",
"origin": {
"Structured": {
"entry_type": "rule",
"origin": "agent"
}
},
"state": "active",
"created_at": "2026-04-08T10:00:00Z",
"last_touched_session": 4,
"tags": [],
"source_ref": null,
"supersedes": []
}
]
}))
.expect("legacy repo runtime json"),
)
.expect("write legacy repo runtime");
let loaded = load_runtime_state(temp.path(), &layout, locality_id)
.expect("load runtime from legacy native repo state");
assert_eq!(
loaded.sources.locality_memory.status,
RuntimeTextSurfaceStatus::LoadedNative
);
assert_eq!(
loaded
.state
.memory
.locality
.iter()
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>(),
vec!["rule: Legacy runtime JSON still loads.".to_owned()]
);
assert!(
load_repo_memory_automation_record(&layout, locality_id, "missing-key")
.expect("load missing automation key")
.is_none()
);
}
#[test]
fn missing_authored_surfaces_become_missing_runtime_sources() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
PathBuf::from("/tmp/repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
let loaded = load_runtime_state(temp.path(), &layout, "ccdrepo_missing")
.expect("missing runtime state should load");
assert_eq!(
loaded.sources.profile_memory.status,
RuntimeTextSurfaceStatus::Missing
);
assert!(loaded.state.memory.profile.is_empty());
assert!(loaded.state.handoff.title.is_empty());
}
#[test]
fn runtime_load_uses_native_handoff_when_markdown_is_missing() {
let temp = tempdir().expect("tempdir");
let repo_root = temp.path().join("repo");
let layout = StateLayout::new(
temp.path().join(".ccd"),
repo_root.join(".git/ccd"),
ProfileName::new("main").expect("profile"),
);
let locality_id = "ccdrepo_runtime";
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
.expect("repo overlay dir");
fs::create_dir_all(layout.clone_profile_root()).expect("clone profile root");
fs::create_dir_all(&repo_root).expect("repo root");
let handoff = RuntimeHandoffState {
title: "Next Session: Native fallback".to_owned(),
immediate_actions: vec![RuntimeHandoffItem {
text: "Use native handoff state.".to_owned(),
lifecycle: RuntimeLifecycle::Active,
}],
..RuntimeHandoffState::default()
};
persist_canonical_handoff_state(&layout, &handoff).expect("persist native handoff");
let loaded =
load_runtime_state(&repo_root, &layout, locality_id).expect("load runtime state");
assert_eq!(
loaded.sources.handoff.status,
RuntimeTextSurfaceStatus::LoadedNative
);
assert_eq!(loaded.state.handoff.title, "Next Session: Native fallback");
assert_eq!(
loaded
.state
.handoff
.immediate_actions
.iter()
.map(|item| item.text.clone())
.collect::<Vec<_>>(),
vec!["Use native handoff state.".to_owned()]
);
}
#[test]
fn runtime_load_accepts_lowercase_native_handoff_lifecycle_values() {
let temp = tempdir().expect("tempdir");
let repo_root = temp.path().join("repo");
let layout = StateLayout::new(
temp.path().join(".ccd"),
repo_root.join(".git/ccd"),
ProfileName::new("main").expect("profile"),
);
let locality_id = "ccdrepo_runtime";
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
.expect("repo overlay dir");
fs::create_dir_all(layout.clone_profile_root()).expect("clone profile root");
fs::create_dir_all(&repo_root).expect("repo root");
fs::create_dir_all(
layout
.clone_runtime_state_path()
.parent()
.expect("clone runtime parent"),
)
.expect("clone runtime dir");
fs::write(
layout.clone_runtime_state_path(),
serde_json::to_string_pretty(&serde_json::json!({
"schema_version": 1,
"handoff_adapter_fingerprint": serde_json::Value::Null,
"handoff": {
"title": "Next Session: Lowercase native lifecycle",
"immediate_actions": [
{ "text": "Accept lowercase lifecycle values.", "lifecycle": "active" }
],
"completed_state": [],
"operational_guardrails": [],
"key_files": [],
"definition_of_done": []
}
}))
.expect("serialize native runtime"),
)
.expect("native runtime");
let loaded =
load_runtime_state(temp.path(), &layout, locality_id).expect("runtime-first load");
assert_eq!(
loaded.sources.handoff.status,
RuntimeTextSurfaceStatus::LoadedNative
);
assert_eq!(
loaded.state.handoff.immediate_actions,
vec![RuntimeHandoffItem {
text: "Accept lowercase lifecycle values.".to_owned(),
lifecycle: RuntimeLifecycle::Active,
}]
);
}
#[test]
fn runtime_load_accepts_legacy_native_profile_state_without_memory_automation() {
let temp = tempdir().expect("tempdir");
let layout = StateLayout::new(
temp.path().join(".ccd"),
temp.path().join("repo/.git/ccd"),
ProfileName::new("main").expect("profile"),
);
fs::create_dir_all(layout.profile_root()).expect("profile root");
fs::write(
layout.profile_runtime_state_path(),
serde_json::to_vec_pretty(&serde_json::json!({
"schema_version": 1,
"memory_adapter_fingerprint": null,
"memory": [
{
"id": "mem_profile_rule",
"text": "Legacy profile runtime JSON still loads.",
"lifecycle": "active",
"origin": {
"Structured": {
"entry_type": "rule",
"origin": "agent"
}
},
"state": "active",
"created_at": "2026-04-08T10:00:00Z",
"last_touched_session": 4,
"tags": [],
"source_ref": null,
"supersedes": []
}
]
}))
.expect("legacy profile runtime json"),
)
.expect("write legacy profile runtime");
let loaded = load_profile_memory_surface(&layout)
.expect("load memory from legacy native profile state");
assert_eq!(loaded.source.status, RuntimeTextSurfaceStatus::LoadedNative);
assert_eq!(
loaded
.entries
.iter()
.map(RuntimeMemoryEntry::projection_text)
.collect::<Vec<_>>(),
vec!["rule: Legacy profile runtime JSON still loads.".to_owned()]
);
assert!(
load_profile_memory_automation_record(&layout, "missing-key")
.expect("load missing profile automation key")
.is_none()
);
}
#[test]
fn render_handoff_markdown_omits_inactive_items_from_export() {
let rendered = render_handoff_markdown(&RuntimeHandoffState {
title: "Next Session: Active export only".to_owned(),
immediate_actions: vec![
RuntimeHandoffItem {
text: "Do the active thing.".to_owned(),
lifecycle: RuntimeLifecycle::Active,
},
RuntimeHandoffItem {
text: "Do not render this archived action.".to_owned(),
lifecycle: RuntimeLifecycle::Inactive,
},
],
completed_state: vec![RuntimeHandoffItem {
text: "Keep this visible.".to_owned(),
lifecycle: RuntimeLifecycle::Active,
}],
operational_guardrails: vec![],
key_files: vec![RuntimeHandoffItem {
text: "`src/archived.rs`".to_owned(),
lifecycle: RuntimeLifecycle::Inactive,
}],
definition_of_done: vec![],
});
assert!(rendered.contains("Do the active thing."));
assert!(rendered.contains("Keep this visible."));
assert!(!rendered.contains("Do not render this archived action."));
assert!(!rendered.contains("`src/archived.rs`"));
assert!(!rendered.contains("## Key Files for This Session"));
}
}