remem-ai 0.6.35

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use axum::{
    body::Bytes,
    extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use rusqlite::TransactionBehavior;
use serde::Serialize;
use serde_json::Value;

use crate::api::mutation::{
    insert_mutation, lookup_mutation, mutation_request_hash, validate_idempotency_key,
    CredentialFreeMutationBody, MutationIdentity, MutationLookup, NewMutationRecord,
    MUTATION_RESPONSE_SCHEMA_VERSION,
};
use crate::memory::governance::{
    govern_memory_for_web_in_transaction, WebMemoryGovernanceAction, WebMemoryGovernanceDecision,
    WebMemoryGovernanceRequest,
};

use super::super::types::{
    DbState, MemorySafeGovernanceRequest, MemorySafeGovernanceResponse, SafeMutationErrorDetail,
    SafeMutationErrorResponse,
};

const RESOURCE_KIND: &str = "memory";
const GOVERNANCE_ACTOR: &str = "api";

struct GovernanceFailure(Box<Response>);

impl From<Response> for GovernanceFailure {
    fn from(response: Response) -> Self {
        Self(Box::new(response))
    }
}

impl GovernanceFailure {
    fn into_response(self) -> Response {
        *self.0
    }
}

type GovernanceResult<T> = Result<T, GovernanceFailure>;

pub(in crate::api) async fn handle_archive_memory(
    State(_state): State<DbState>,
    Path(id): Path<String>,
    body: Bytes,
) -> Response {
    parse_request(&body)
        .and_then(|(identity, request)| {
            execute_governance(id, WebMemoryGovernanceAction::Archive, identity, request)
        })
        .unwrap_or_else(GovernanceFailure::into_response)
}

pub(in crate::api) async fn handle_restore_memory(
    State(_state): State<DbState>,
    Path(id): Path<String>,
    body: Bytes,
) -> Response {
    parse_request(&body)
        .and_then(|(identity, request)| {
            execute_governance(id, WebMemoryGovernanceAction::Restore, identity, request)
        })
        .unwrap_or_else(GovernanceFailure::into_response)
}

#[derive(Serialize)]
struct GovernanceHashBody<'a> {
    reason: &'a str,
    expected_version: i64,
}

impl CredentialFreeMutationBody for GovernanceHashBody<'_> {}

fn parse_request(body: &[u8]) -> GovernanceResult<(MutationIdentity, MemorySafeGovernanceRequest)> {
    let value: Value = serde_json::from_slice(body).map_err(|_| {
        safe_error(
            StatusCode::BAD_REQUEST,
            "memory_governance_request_invalid",
            "request body must be valid JSON",
            None,
        )
    })?;
    let raw_key = value
        .as_object()
        .and_then(|object| object.get("idempotency_key"))
        .and_then(Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| {
            safe_error(
                StatusCode::BAD_REQUEST,
                "idempotency_key_invalid",
                "idempotency key is required",
                None,
            )
        })?;
    let identity = validate_idempotency_key(&raw_key).map_err(|_| {
        safe_error(
            StatusCode::BAD_REQUEST,
            "idempotency_key_invalid",
            "idempotency key is invalid",
            None,
        )
    })?;
    let operation_id = Some(identity.operation_id.as_str());
    let mut request: MemorySafeGovernanceRequest = serde_json::from_value(value).map_err(|_| {
        safe_error(
            StatusCode::BAD_REQUEST,
            "memory_governance_request_invalid",
            "request fields are invalid",
            operation_id,
        )
    })?;
    request.reason = request.reason.trim().to_string();
    request.idempotency_key.clear();
    if request.reason.is_empty() || request.reason.len() > 1024 {
        return Err(safe_error(
            StatusCode::BAD_REQUEST,
            "reason_invalid",
            "reason must contain between 1 and 1024 UTF-8 bytes",
            operation_id,
        )
        .into());
    }
    if request.expected_version <= 0 {
        return Err(safe_error(
            StatusCode::BAD_REQUEST,
            "memory_governance_request_invalid",
            "expected_version must be positive",
            operation_id,
        )
        .into());
    }
    Ok((identity, request))
}

fn execute_governance(
    raw_id: String,
    action: WebMemoryGovernanceAction,
    identity: MutationIdentity,
    request: MemorySafeGovernanceRequest,
) -> GovernanceResult<Response> {
    let memory_id = parse_memory_id(&raw_id, &identity)?;
    let mut conn = crate::db::open_db()
        .map_err(|_| safe_internal_error("db_open_failed", Some(&identity.operation_id)))?;
    execute_governance_on_connection(&mut conn, memory_id, action, identity, request)
}

fn parse_memory_id(raw_id: &str, identity: &MutationIdentity) -> GovernanceResult<i64> {
    match raw_id.parse::<i64>() {
        Ok(id) if id > 0 => Ok(id),
        _ => Err(safe_error(
            StatusCode::BAD_REQUEST,
            "id_invalid",
            "memory id must be a positive integer",
            Some(&identity.operation_id),
        )
        .into()),
    }
}

fn execute_governance_on_connection(
    conn: &mut rusqlite::Connection,
    memory_id: i64,
    action: WebMemoryGovernanceAction,
    identity: MutationIdentity,
    request: MemorySafeGovernanceRequest,
) -> GovernanceResult<Response> {
    let request_hash = mutation_request_hash(
        RESOURCE_KIND,
        memory_id,
        action.as_str(),
        &GovernanceHashBody {
            reason: &request.reason,
            expected_version: request.expected_version,
        },
    )
    .map_err(|_| {
        safe_internal_error(
            "memory_governance_hash_failed",
            Some(&identity.operation_id),
        )
    })?;
    let tx = conn
        .transaction_with_behavior(TransactionBehavior::Immediate)
        .map_err(|_| {
            safe_internal_error(
                "memory_governance_transaction_failed",
                Some(&identity.operation_id),
            )
        })?;

    match lookup_mutation(&tx, &identity, &request_hash).map_err(|_| {
        safe_internal_error(
            "memory_governance_ledger_failed",
            Some(&identity.operation_id),
        )
    })? {
        MutationLookup::Replay(stored) => {
            let mut response: MemorySafeGovernanceResponse =
                serde_json::from_str(&stored.response_json).map_err(|_| {
                    safe_internal_error(
                        "memory_governance_replay_invalid",
                        Some(&identity.operation_id),
                    )
                })?;
            response.replayed = true;
            tx.commit().map_err(|_| {
                safe_internal_error(
                    "memory_governance_transaction_failed",
                    Some(&identity.operation_id),
                )
            })?;
            return Ok(Json(response).into_response());
        }
        MutationLookup::Conflict => {
            return Err(safe_error(
                StatusCode::CONFLICT,
                "idempotency_conflict",
                "idempotency key was already used for a different request",
                Some(&identity.operation_id),
            )
            .into())
        }
        MutationLookup::UnsupportedSchema(_) => {
            return Err(safe_error(
                StatusCode::CONFLICT,
                "idempotency_schema_unsupported",
                "stored response schema is not supported",
                Some(&identity.operation_id),
            )
            .into())
        }
        MutationLookup::Miss => {}
    }

    let decision = govern_memory_for_web_in_transaction(
        &tx,
        &WebMemoryGovernanceRequest {
            memory_id,
            action,
            expected_version: request.expected_version,
            operation_id: &identity.operation_id,
            reason: &request.reason,
            actor: GOVERNANCE_ACTOR,
        },
    )
    .map_err(|_| {
        safe_internal_error(
            "memory_governance_mutation_failed",
            Some(&identity.operation_id),
        )
    })?;
    let applied = match decision {
        WebMemoryGovernanceDecision::Applied(applied) => applied,
        WebMemoryGovernanceDecision::NotFound => {
            return Err(safe_error(
                StatusCode::NOT_FOUND,
                "memory_not_found",
                "memory was not found",
                Some(&identity.operation_id),
            )
            .into())
        }
        WebMemoryGovernanceDecision::VersionConflict => {
            return Err(safe_error(
                StatusCode::CONFLICT,
                "version_conflict",
                "memory version does not match expected_version",
                Some(&identity.operation_id),
            )
            .into())
        }
        WebMemoryGovernanceDecision::NotArchivable => {
            return Err(safe_error(
                StatusCode::CONFLICT,
                "memory_not_archivable",
                "memory is not active and cannot be archived",
                Some(&identity.operation_id),
            )
            .into())
        }
        WebMemoryGovernanceDecision::NotRecoverable => {
            return Err(safe_error(
                StatusCode::NOT_FOUND,
                "memory_not_recoverable",
                "memory does not have current Web archive provenance",
                Some(&identity.operation_id),
            )
            .into())
        }
    };
    let response = MemorySafeGovernanceResponse {
        response_schema_version: MUTATION_RESPONSE_SCHEMA_VERSION,
        operation_id: identity.operation_id.clone(),
        audit_id: applied.audit_id,
        memory_id,
        action: action.as_str().to_string(),
        before_status: applied.before_status,
        after_status: applied.after_status,
        version: applied.version,
        occurred_at_epoch: applied.occurred_at_epoch,
        replayed: false,
    };
    let response_json = serde_json::to_string(&response).map_err(|_| {
        safe_internal_error(
            "memory_governance_response_failed",
            Some(&identity.operation_id),
        )
    })?;
    insert_mutation(
        &tx,
        &NewMutationRecord {
            identity: &identity,
            request_hash: &request_hash,
            resource_kind: RESOURCE_KIND,
            resource_id: memory_id,
            action: action.as_str(),
            response_json: &response_json,
            audit_id: applied.audit_id,
            created_at_epoch: applied.occurred_at_epoch,
        },
    )
    .map_err(|_| {
        safe_internal_error(
            "memory_governance_ledger_failed",
            Some(&identity.operation_id),
        )
    })?;
    tx.commit().map_err(|_| {
        safe_internal_error(
            "memory_governance_transaction_failed",
            Some(&identity.operation_id),
        )
    })?;
    Ok(Json(response).into_response())
}

#[cfg(test)]
pub(in crate::api) fn execute_memory_governance_for_test(
    conn: &mut rusqlite::Connection,
    memory_id: i64,
    action: &str,
    body: &[u8],
) -> Response {
    let action = match action {
        "archive" => WebMemoryGovernanceAction::Archive,
        "restore" => WebMemoryGovernanceAction::Restore,
        _ => {
            return safe_error(
                StatusCode::BAD_REQUEST,
                "action_invalid",
                "action is invalid",
                None,
            )
        }
    };
    parse_request(body)
        .and_then(|(identity, request)| {
            execute_governance_on_connection(conn, memory_id, action, identity, request)
        })
        .unwrap_or_else(GovernanceFailure::into_response)
}

fn safe_internal_error(code: &str, operation_id: Option<&str>) -> Response {
    safe_error(
        StatusCode::INTERNAL_SERVER_ERROR,
        code,
        "memory governance request failed",
        operation_id,
    )
}

fn safe_error(
    status: StatusCode,
    code: &str,
    message: &str,
    operation_id: Option<&str>,
) -> Response {
    (
        status,
        Json(SafeMutationErrorResponse {
            error: SafeMutationErrorDetail {
                code: code.to_string(),
                message: message.to_string(),
                operation_id: operation_id.map(str::to_string),
            },
        }),
    )
        .into_response()
}