use memory::backend::{ MemoryBackendAckOutput, MemoryBackendOperation, MemoryBackendOperationResult, MemoryDocumentReadOperation, MemoryDocumentUpdateOperation, MemoryQueryOperation, MemoryStagingCloseAction, MemoryStagingCloseOperation, MemoryStagingListOperation, MemoryStagingReadOperation, MemoryStagingWriteOutput, MemoryToolOutput, }; use memory::extract::{ExtractedCandidate, StagingEvidence, StagingRecord}; use memory::schema::{SourceEvidenceRef, SourceRef}; use serde_json::json; use uuid::Uuid; use crate::authority::MemoryAuthority; use crate::{Error, Result}; const DEFAULT_READ_LIMIT: usize = 2000; const MAX_STAGING_LIST_LIMIT: usize = 100; pub fn execute_memory_backend_operation_with_authority( authority: &A, operation: MemoryBackendOperation, ) -> Result { match operation { MemoryBackendOperation::Query(operation) => { execute_query(authority, operation).map(MemoryBackendOperationResult::ToolOutput) } MemoryBackendOperation::ReadDocument(operation) => { execute_read_document(authority, operation) .map(MemoryBackendOperationResult::ToolOutput) } MemoryBackendOperation::UpdateDocument(operation) => { execute_update_document(authority, operation) .map(MemoryBackendOperationResult::ToolOutput) } MemoryBackendOperation::ResidentSummary(_operation) => Ok( MemoryBackendOperationResult::ToolOutput(execute_resident_summary(authority)?), ), MemoryBackendOperation::AppendAudit(_operation) => Ok( MemoryBackendOperationResult::Acknowledged(MemoryBackendAckOutput { summary: "memory audit event accepted by Workspace authority".to_string(), }), ), MemoryBackendOperation::StageCandidate(operation) => { let id = stage_candidate( authority, operation.source, operation.extract_run_id, operation.candidate, operation.evidence, operation.source_refs, )?; Ok(MemoryBackendOperationResult::StagingWritten( MemoryStagingWriteOutput { staging_count: 1, staging_ids: vec![id], }, )) } MemoryBackendOperation::StageExtracted(operation) => { let extract_run_id = Uuid::now_v7().to_string(); let mut ids = Vec::with_capacity(operation.payload.candidates.len()); for candidate in operation.payload.candidates { ids.push(stage_candidate( authority, operation.source.clone(), extract_run_id.clone(), candidate, Vec::new(), Vec::new(), )?); } Ok(MemoryBackendOperationResult::StagingWritten( MemoryStagingWriteOutput { staging_count: ids.len(), staging_ids: ids, }, )) } MemoryBackendOperation::StagingList(operation) => { execute_staging_list(authority, operation).map(MemoryBackendOperationResult::ToolOutput) } MemoryBackendOperation::StagingRead(operation) => { execute_staging_read(authority, operation).map(MemoryBackendOperationResult::ToolOutput) } MemoryBackendOperation::StagingClose(operation) => { execute_staging_close(authority, operation) .map(MemoryBackendOperationResult::ToolOutput) } } } fn execute_resident_summary(authority: &A) -> Result { let document = authority.ensure_memory_document()?; if document.body_md.trim().is_empty() { Ok(MemoryToolOutput { summary: "resident memory summary unavailable".to_string(), content: None, }) } else { Ok(MemoryToolOutput { summary: "resident memory document collected".to_string(), content: Some(document.body_md), }) } } fn execute_query( authority: &A, operation: MemoryQueryOperation, ) -> Result { let document = authority.ensure_memory_document()?; let query = operation .query .as_deref() .map(str::trim) .filter(|query| !query.is_empty()); match query { Some(query) => { if let Some(body) = excerpt_matching(&document.body_md, query) { Ok(MemoryToolOutput { summary: format!("Memory document matched {query:?}"), content: Some(body), }) } else { Ok(MemoryToolOutput { summary: format!("No memory document content matched {query:?}"), content: None, }) } } None => Ok(MemoryToolOutput { summary: "Memory document overview".to_string(), content: Some(excerpt_head(&document.body_md, 120)), }), } } fn execute_read_document( authority: &A, operation: MemoryDocumentReadOperation, ) -> Result { let document = authority.ensure_memory_document()?; let offset = operation.offset.unwrap_or(0); let limit = operation.limit.unwrap_or(DEFAULT_READ_LIMIT); let lines: Vec<&str> = document.body_md.lines().collect(); let selected = lines .iter() .enumerate() .skip(offset) .take(limit) .map(|(index, line)| format!("{:>6}\t{}", index + 1, line)) .collect::>() .join("\n"); let total = lines.len(); Ok(MemoryToolOutput { summary: format!( "Read {} lines from Workspace memory document", selected.lines().count() ), content: Some(format!( "total_lines={total} offset={offset} limit={limit}\n{selected}" )), }) } fn execute_update_document( authority: &A, operation: MemoryDocumentUpdateOperation, ) -> Result { if operation.old_string == operation.new_string { return Err(Error::Store( "memory document old_string and new_string must differ".to_string(), )); } if operation.old_string.is_empty() { return Err(Error::Store( "memory document old_string must not be empty".to_string(), )); } let document = authority.ensure_memory_document()?; let match_count = document.body_md.matches(&operation.old_string).count(); if match_count == 0 { return Err(Error::Store( "memory document old_string was not found".to_string(), )); } if match_count > 1 && !operation.replace_all { return Err(Error::Store( "memory document old_string matched more than once; set replace_all=true to replace every occurrence".to_string(), )); } let updated_body = if operation.replace_all { document .body_md .replace(&operation.old_string, &operation.new_string) } else { document .body_md .replacen(&operation.old_string, &operation.new_string, 1) }; if updated_body.trim().is_empty() { return Err(Error::Store( "memory document body must not become empty".to_string(), )); } let updated = authority.update_memory_document(&updated_body)?; Ok(MemoryToolOutput { summary: format!( "Edited {} occurrence(s) in Workspace memory document", if operation.replace_all { match_count } else { 1 } ), content: Some(format!( "updated_at={} bytes={}", updated.updated_at, updated.body_md.len() )), }) } fn stage_candidate( authority: &A, source: SourceRef, extract_run_id: String, candidate: ExtractedCandidate, evidence: Vec, source_refs: Vec, ) -> Result { let candidate_id = Uuid::now_v7().to_string(); let record = StagingRecord::from_candidate( candidate_id.clone(), extract_run_id, source, candidate, evidence, source_refs, ); let raw_json = serde_json::to_string_pretty(&record) .map_err(|err| Error::Store(format!("serialize memory staging record: {err}")))?; authority.upsert_memory_staging_record(&candidate_id, &raw_json, None)?; Ok(candidate_id) } fn execute_staging_list( authority: &A, operation: MemoryStagingListOperation, ) -> Result { let limit = operation.limit.unwrap_or(20).min(MAX_STAGING_LIST_LIMIT); let entries = authority.list_memory_staging_records(limit)?; let records = entries .iter() .map(|entry| { staging_summary_json( &entry.candidate_id, &entry.raw_json, entry.source_path.as_deref(), ) }) .collect::>>()?; Ok(MemoryToolOutput { summary: format!( "Listed {} memory staging candidate(s) from Workspace authority", records.len() ), content: Some( serde_json::to_string_pretty(&records) .map_err(|err| Error::Store(format!("serialize memory staging list: {err}")))?, ), }) } fn execute_staging_read( authority: &A, operation: MemoryStagingReadOperation, ) -> Result { let entry = authority.memory_staging_record(&operation.candidate_id)?; let raw: serde_json::Value = serde_json::from_str(&entry.raw_json) .map_err(|err| Error::Store(format!("decode memory staging record: {err}")))?; Ok(MemoryToolOutput { summary: format!("Read memory staging candidate {}", entry.candidate_id), content: Some( serde_json::to_string_pretty(&raw) .map_err(|err| Error::Store(format!("serialize memory staging record: {err}")))?, ), }) } fn execute_staging_close( authority: &A, operation: MemoryStagingCloseOperation, ) -> Result { if operation.reason.trim().is_empty() { return Err(Error::Store("reason is required".to_string())); } let affected_refs_json = serde_json::to_string(&operation.affected_memory) .map_err(|err| Error::Store(format!("serialize affected memory refs: {err}")))?; let action = staging_close_action_name(&operation.action); let resolution = authority.close_memory_staging_record( &operation.candidate_id, action, &operation.reason, &affected_refs_json, )?; Ok(MemoryToolOutput { summary: format!("Closed memory staging candidate {}", resolution.candidate_id), content: Some( serde_json::to_string_pretty(&json!({ "candidate_id": resolution.candidate_id, "action": resolution.action, "reason": resolution.reason, "affected_refs": serde_json::from_str::(&resolution.affected_refs_json).unwrap_or_else(|_| json!([])), "resolved_at": resolution.resolved_at, "record_authority": resolution.record_source, })) .map_err(|err| Error::Store(format!("serialize memory staging resolution: {err}")))?, ), }) } fn staging_close_action_name(action: &MemoryStagingCloseAction) -> &'static str { match action { MemoryStagingCloseAction::Applied => "applied", MemoryStagingCloseAction::Discarded => "discarded", MemoryStagingCloseAction::Invalid => "invalid", MemoryStagingCloseAction::Duplicate => "duplicate", MemoryStagingCloseAction::AlreadyCovered => "already_covered", } } fn staging_summary_json( candidate_id: &str, raw_json: &str, source_path: Option<&str>, ) -> Result { let raw: serde_json::Value = serde_json::from_str(raw_json).map_err(|err| { Error::Store(format!( "decode memory staging record {candidate_id}: {err}" )) })?; Ok(json!({ "candidate_id": candidate_id, "bytes": raw_json.len(), "source_path": source_path, "source": raw.get("source").cloned().unwrap_or(serde_json::Value::Null), "kind": raw.get("kind").cloned().unwrap_or(serde_json::Value::Null), "claim": raw.get("claim").cloned().unwrap_or(serde_json::Value::Null), "why_useful": raw.get("why_useful").cloned().unwrap_or(serde_json::Value::Null), "staleness": raw.get("staleness").cloned().unwrap_or(serde_json::Value::Null), "evidence_count": raw.get("evidence").and_then(|value| value.as_array()).map_or(0, Vec::len), "source_ref_count": raw.get("source_refs").and_then(|value| value.as_array()).map_or(0, Vec::len), })) } fn excerpt_head(body: &str, max_lines: usize) -> String { let mut lines = body.lines().take(max_lines).collect::>().join("\n"); if body.lines().count() > max_lines { lines.push_str("\n... [truncated]"); } lines } fn excerpt_matching(body: &str, query: &str) -> Option { let query_lower = query.to_lowercase(); let lines = body.lines().collect::>(); let index = lines .iter() .position(|line| line.to_lowercase().contains(&query_lower))?; let start = index.saturating_sub(3); let end = (index + 4).min(lines.len()); Some( lines[start..end] .iter() .enumerate() .map(|(offset, line)| format!("{:>6}\t{}", start + offset + 1, line)) .collect::>() .join("\n"), ) } #[cfg(test)] mod tests { use super::*; use crate::authority::SqliteWorkspaceAuthority; use memory::backend::{ MemoryDocumentReadOperation, MemoryDocumentUpdateOperation, MemoryStagingCloseAction, MemoryStagingCloseOperation, MemoryStagingListOperation, }; use memory::extract::{CandidateKind, ExtractedCandidate}; use memory::schema::SourceRef; use tempfile::TempDir; fn authority(temp: &TempDir) -> SqliteWorkspaceAuthority { let db_path = temp.path().join("workspace.sqlite3"); let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace").unwrap(); let conn = rusqlite::Connection::open(&db_path).unwrap(); conn.execute( "INSERT INTO workspaces ( workspace_id, owner_account_id, display_name, state, created_at, updated_at ) VALUES (?1, NULL, ?2, ?3, ?4, ?4)", rusqlite::params!["workspace", "Workspace", "active", "2026-01-01T00:00:00Z"], ) .unwrap(); authority } fn source() -> SourceRef { SourceRef { segment_id: "segment-1".into(), range: [1, 2], } } fn candidate() -> ExtractedCandidate { ExtractedCandidate { kind: CandidateKind::Decision, claim: "Use SQLite memory authority".into(), why_useful: "Keeps Worker tools away from .yoi/memory filesystem authority".into(), staleness: None, evidence_ids: Vec::new(), } } #[test] fn document_operations_use_workspace_authority() { let temp = TempDir::new().unwrap(); let authority = authority(&temp); execute_memory_backend_operation_with_authority( &authority, MemoryBackendOperation::UpdateDocument(MemoryDocumentUpdateOperation { old_string: "# Memory\n\n".into(), new_string: "# Memory\n\nSQLite backed body".into(), replace_all: false, }), ) .unwrap(); let result = execute_memory_backend_operation_with_authority( &authority, MemoryBackendOperation::ReadDocument(MemoryDocumentReadOperation { offset: None, limit: None, }), ) .unwrap(); let MemoryBackendOperationResult::ToolOutput(output) = result else { panic!("expected tool output") }; assert!(output.content.unwrap().contains("SQLite backed body")); } #[test] fn staging_close_moves_record_to_workspace_resolution() { let temp = TempDir::new().unwrap(); let authority = authority(&temp); let result = execute_memory_backend_operation_with_authority( &authority, MemoryBackendOperation::StageCandidate( memory::backend::MemoryStageCandidateOperation { source: source(), extract_run_id: "run-1".into(), candidate: candidate(), evidence: Vec::new(), source_refs: Vec::new(), }, ), ) .unwrap(); let MemoryBackendOperationResult::StagingWritten(written) = result else { panic!("expected staging write") }; assert_eq!(written.staging_count, 1); let list = execute_memory_backend_operation_with_authority( &authority, MemoryBackendOperation::StagingList(MemoryStagingListOperation { limit: None }), ) .unwrap(); let MemoryBackendOperationResult::ToolOutput(list) = list else { panic!("expected list") }; assert!( list.content .unwrap() .contains("Use SQLite memory authority") ); execute_memory_backend_operation_with_authority( &authority, MemoryBackendOperation::StagingClose(MemoryStagingCloseOperation { candidate_id: written.staging_ids[0].clone(), action: MemoryStagingCloseAction::Applied, reason: "merged into document".into(), affected_memory: Vec::new(), }), ) .unwrap(); assert_eq!(authority.list_memory_staging_records(10).unwrap().len(), 0); assert_eq!( authority.list_memory_staging_resolutions(10).unwrap().len(), 1 ); } }