28 KiB
CMS Wiring: Conversation + Rewind Blob Store Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the SQLite-backed messages/archives/blobs tables (crates/zesdex-backend/src/model/msglog/) with zesdex-cms's Conversation/JsonConversationRepository for message content, plus a brand-new file-based blob repository for rewind snapshots (no zesdex-cms equivalent existed before this plan). Fix the ChatMessage type collision between zesdex-cms's local duplicate and the canonical zesdex_entities::seaorm::common::message::ChatMessage used everywhere else in the codebase.
Architecture: Research confirmed the messages table is write-only today (archived but never read back to restore a session) and the archives table is created but never populated by any code path — both can be retired with zero behavior loss. The blobs table is the one genuinely load-bearing piece (rewind feature reads it back) and needs a real, tested replacement — a new RewindBlobRepository trait + FileRewindBlobRepository impl added to zesdex-cms, storing raw bytes as <session_dir>/blobs/<hex(key)>.bin plus an append-only <session_dir>/blobs/index.jsonl for key/mime_type/ordering metadata (mirroring the JSONL-index pattern zesdex-cms's own EditLogRepository already uses).
Tech Stack: Rust, Cargo workspace (zesdex-cms, zesdex-backend, zesdex-entities).
Global Constraints
- No
#[allow(...)]additions beyond what's already in touched files. - Tests are inline
#[cfg(test)] mod tests. - Run
cargo test --workspaceandcargo clippy --workspace --all-targets -- -D warningsbefore each commit. - This plan deliberately does not attempt to migrate historical data out of any existing
messages.sqlitefiles — since themessages/archivestables were never read back by any code path, there is nothing meaningful to migrate. Existingmessages.sqlitefiles are simply left on disk, unused, after this plan (a future cleanup could delete them, but doing so isn't required for correctness).
Task 1: Reconcile the ChatMessage/Role type collision in zesdex-cms
Context: crates/zesdex-cms/src/domain/conversation.rs currently defines its own Role/ChatMessage (with tool_calls: Option<Vec<serde_json::Value>>, untyped) instead of reusing zesdex_entities::seaorm::common::message::{Role, ChatMessage} (the canonical type used throughout zesdex-backend, with tool_calls: Option<Vec<ToolCall>>, strongly typed). zesdex-cms already depends on zesdex-entities (confirmed in Cargo.toml), so this is a small, surgical fix.
Files:
- Modify:
crates/zesdex-cms/src/domain/conversation.rs - Modify:
crates/zesdex-cms/src/application/conversation_service.rs(import path only) - Modify:
crates/zesdex-cms/src/domain/service.rs(import path only)
Interfaces:
-
Produces:
zesdex_cms::domain::conversation::{Conversation, ChatMessage, Role}whereChatMessage/Roleare now re-exports of the canonical entities type — anything constructing azesdex_cms::domain::conversation::ChatMessageis now interchangeable withzesdex_entities::seaorm::common::message::ChatMessageused elsewhere inzesdex-backend. -
Step 1: Write the failing test proving type interchangeability
Add to crates/zesdex-cms/src/domain/conversation.rs's #[cfg(test)] mod tests (create if absent):
#[test]
fn chat_message_is_the_canonical_entities_type() {
// This is a compile-time proof more than a runtime assertion: if
// `zesdex_cms::domain::conversation::ChatMessage` were still a
// distinct local type, this line would fail to compile.
let canonical = zesdex_entities::seaorm::common::message::ChatMessage::user("hi");
let via_cms: ChatMessage = canonical;
assert_eq!(via_cms.content.as_deref(), Some("hi"));
}
- Step 2: Run the test to verify it fails
Run: cargo test -p zesdex-cms chat_message_is_the_canonical -- --nocapture
Expected: compile error — serde_json::Value (cms's old tool_calls field type) vs ChatMessage::user's type won't unify, or a straightforward type mismatch.
- Step 3: Replace the local
Role/ChatMessagewith re-exports
In crates/zesdex-cms/src/domain/conversation.rs, delete the entire local Role enum and ChatMessage struct + impl block (the definitions, constructors user/assistant/system/tool), and replace the top of the file with:
//! Pure Conversation entity — in-memory message history plus system prompt
//! and LLM generation parameters.
//!
//! # Architecture
//! This is a pure data structure with **no I/O logic**. Load/save
//! responsibilities live in [`ConversationRepository`](super::repository::ConversationRepository).
//!
//! `ChatMessage`/`Role` are re-exported from `zesdex-entities` rather than
//! duplicated here, so a `Conversation` built by this crate is
//! interchangeable with the `ChatMessage` type used throughout
//! `zesdex-backend`'s provider/tool-execution layer.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
use serde::{Deserialize, Serialize};
pub use zesdex_entities::seaorm::common::message::{ChatMessage, Role};
Keep the Conversation struct, impl Conversation block (new, push, rebuild_system, to_api_messages, len, is_empty) unchanged below this — they only reference ChatMessage/Role by name, which now resolve to the re-exported canonical types.
- Step 4: Fix any now-broken references in the same crate
Run: cargo build -p zesdex-cms 2>&1 | head -60
If application/conversation_service.rs or domain/service.rs import ChatMessage/Role via use super::conversation::{ChatMessage, Conversation} or similar — these continue to work unchanged since the names are still exported from domain::conversation, just backed by a different underlying type now. Only fix compile errors that actually appear; do not preemptively touch files the build doesn't flag.
- Step 5: Run the test to verify it passes
Run: cargo test -p zesdex-cms chat_message_is_the_canonical -- --nocapture
Expected: pass.
- Step 6: Run the crate's full test suite and clippy
Run: cargo test -p zesdex-cms && cargo clippy -p zesdex-cms -- -D warnings
Expected: all pass.
- Step 7: Commit
git add crates/zesdex-cms
git commit -m "fix(cms): satukan ChatMessage/Role Conversation dengan tipe kanonik zesdex-entities"
Task 2: Add RewindBlobRepository to zesdex-cms
Files:
- Modify:
crates/zesdex-cms/src/domain/repository.rs - Create:
crates/zesdex-cms/src/infrastructure/persistence/rewind_blob_repo.rs - Modify:
crates/zesdex-cms/src/infrastructure/persistence/mod.rs - Modify:
crates/zesdex-cms/Cargo.toml(addhexandchronoif not already present —chronois already a dependency per the crate's existingCargo.toml; confirmhexwithgrep hex crates/zesdex-cms/Cargo.tomland addhex.workspace = trueif missing)
Interfaces:
-
Produces:
pub trait RewindBlobRepository { fn store_blob(&self, session_dir: &Path, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> Result<()>; fn retrieve_blob(&self, session_dir: &Path, blob_key: &str) -> Result<Option<Vec<u8>>>; fn list_blob_keys(&self, session_dir: &Path) -> Result<Vec<String>>; }andpub struct FileRewindBlobRepository— used by Task 4. -
Step 1: Add the trait
In crates/zesdex-cms/src/domain/repository.rs, add:
/// Repository for rewind-snapshot binary blobs, keyed by an arbitrary
/// caller-supplied key (e.g. a tool-call id) within a session.
pub trait RewindBlobRepository {
/// Store (or overwrite) a blob under `blob_key` for this session.
fn store_blob(&self, session_dir: &Path, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> anyhow::Result<()>;
/// Retrieve a blob's bytes by key, or `None` if not found.
fn retrieve_blob(&self, session_dir: &Path, blob_key: &str) -> anyhow::Result<Option<Vec<u8>>>;
/// List all blob keys for this session, oldest first.
fn list_blob_keys(&self, session_dir: &Path) -> anyhow::Result<Vec<String>>;
}
- Step 2: Write the failing tests
Create crates/zesdex-cms/src/infrastructure/persistence/rewind_blob_repo.rs with:
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("zesdex-cms-blob-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn store_and_retrieve_roundtrip() {
let dir = tmp_dir();
let repo = FileRewindBlobRepository::new();
repo.store_blob(&dir, "tool-call-1", b"hello world", Some("text/plain")).unwrap();
let bytes = repo.retrieve_blob(&dir, "tool-call-1").unwrap();
assert_eq!(bytes, Some(b"hello world".to_vec()));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn retrieve_missing_key_returns_none() {
let dir = tmp_dir();
let repo = FileRewindBlobRepository::new();
assert_eq!(repo.retrieve_blob(&dir, "no-such-key").unwrap(), None);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_blob_keys_returns_oldest_first() {
let dir = tmp_dir();
let repo = FileRewindBlobRepository::new();
repo.store_blob(&dir, "first", b"a", None).unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
repo.store_blob(&dir, "second", b"b", None).unwrap();
let keys = repo.list_blob_keys(&dir).unwrap();
assert_eq!(keys, vec!["first".to_string(), "second".to_string()]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn overwriting_a_key_keeps_only_the_latest_entry_in_the_listing() {
let dir = tmp_dir();
let repo = FileRewindBlobRepository::new();
repo.store_blob(&dir, "k", b"v1", None).unwrap();
repo.store_blob(&dir, "k", b"v2", None).unwrap();
let keys = repo.list_blob_keys(&dir).unwrap();
assert_eq!(keys, vec!["k".to_string()], "key must appear exactly once even after being overwritten");
assert_eq!(repo.retrieve_blob(&dir, "k").unwrap(), Some(b"v2".to_vec()));
let _ = std::fs::remove_dir_all(&dir);
}
}
(Requires uuid — already a zesdex-cms dependency per its Cargo.toml.)
- Step 3: Run the tests to verify they fail
Run: cargo test -p zesdex-cms rewind_blob_repo:: 2>&1 | head -20
Expected: compile error (FileRewindBlobRepository doesn't exist yet).
- Step 4: Implement
FileRewindBlobRepository
Add above the test module in the same file:
//! Filesystem-backed `RewindBlobRepository` implementation.
//!
//! Blob bytes are stored at `<session_dir>/blobs/<hex(key)>.bin` (the key
//! is hex-encoded as the filename to sidestep any path-traversal/invalid-
//! filename-character concerns entirely, mirroring the simplicity of
//! `Memory::slugify` elsewhere in this crate but without needing a
//! human-readable filename). Key/ordering/mime-type metadata lives in an
//! append-only `<session_dir>/blobs/index.jsonl`, one JSON line per
//! `store_blob` call — the same JSONL-index pattern already used by
//! `EditLogRepository`. `list_blob_keys` de-duplicates by keeping each
//! key's *last* index line (so overwriting a key doesn't produce a
//! duplicate listing entry) and returns keys ordered by first-seen
//! `created_at` ascending (oldest first), matching the previous
//! `SQLite`-backed `ORDER BY created_at ASC` behavior.
use std::io::Write;
use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::domain::repository::RewindBlobRepository;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct BlobIndexEntry {
key: String,
mime_type: Option<String>,
created_at: i64,
}
/// Concrete filesystem rewind-blob repository.
#[derive(Debug, Clone, Default)]
pub struct FileRewindBlobRepository;
impl FileRewindBlobRepository {
/// Create a new filesystem rewind-blob repository.
pub fn new() -> Self {
Self
}
fn blobs_dir(session_dir: &Path) -> std::path::PathBuf {
session_dir.join("blobs")
}
fn blob_file_path(session_dir: &Path, blob_key: &str) -> std::path::PathBuf {
Self::blobs_dir(session_dir).join(format!("{}.bin", hex::encode(blob_key.as_bytes())))
}
fn index_path(session_dir: &Path) -> std::path::PathBuf {
Self::blobs_dir(session_dir).join("index.jsonl")
}
}
impl RewindBlobRepository for FileRewindBlobRepository {
fn store_blob(&self, session_dir: &Path, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> Result<()> {
let blobs_dir = Self::blobs_dir(session_dir);
std::fs::create_dir_all(&blobs_dir)
.with_context(|| format!("failed to create blobs dir '{}'", blobs_dir.display()))?;
let path = Self::blob_file_path(session_dir, blob_key);
let tmp = path.with_extension("bin.tmp");
std::fs::write(&tmp, data)?;
let f = std::fs::File::open(&tmp)?;
f.sync_all()?;
std::fs::rename(&tmp, &path)?;
let entry = BlobIndexEntry {
key: blob_key.to_string(),
mime_type: mime_type.map(String::from),
created_at: chrono::Utc::now().timestamp_millis(),
};
let index_path = Self::index_path(session_dir);
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&index_path)
.with_context(|| format!("failed to open blob index '{}'", index_path.display()))?;
writeln!(f, "{}", serde_json::to_string(&entry)?)?;
f.sync_all()?;
Ok(())
}
fn retrieve_blob(&self, session_dir: &Path, blob_key: &str) -> Result<Option<Vec<u8>>> {
let path = Self::blob_file_path(session_dir, blob_key);
if !path.exists() {
return Ok(None);
}
Ok(Some(std::fs::read(&path)?))
}
fn list_blob_keys(&self, session_dir: &Path) -> Result<Vec<String>> {
let index_path = Self::index_path(session_dir);
let Ok(content) = std::fs::read_to_string(&index_path) else {
return Ok(Vec::new());
};
// Keep only the last occurrence of each key (later overwrites win),
// but remember first-seen order for the final ascending sort.
let mut first_seen_order: Vec<String> = Vec::new();
let mut latest_by_key: std::collections::HashMap<String, BlobIndexEntry> = std::collections::HashMap::new();
for line in content.lines() {
let Ok(entry) = serde_json::from_str::<BlobIndexEntry>(line) else {
continue;
};
if !latest_by_key.contains_key(&entry.key) {
first_seen_order.push(entry.key.clone());
}
latest_by_key.insert(entry.key.clone(), entry);
}
let mut entries: Vec<BlobIndexEntry> = first_seen_order
.into_iter()
.filter_map(|k| latest_by_key.get(&k).cloned())
.collect();
entries.sort_by_key(|e| e.created_at);
Ok(entries.into_iter().map(|e| e.key).collect())
}
}
- Step 5: Register the module
In crates/zesdex-cms/src/infrastructure/persistence/mod.rs, add:
pub mod rewind_blob_repo;
- Step 6: Run the tests to verify they pass
Run: cargo test -p zesdex-cms rewind_blob_repo:: -- --nocapture
Expected: all 4 tests pass.
- Step 7: Run clippy
Run: cargo clippy -p zesdex-cms -- -D warnings
Expected: no new warnings.
- Step 8: Commit
git add crates/zesdex-cms/src/domain/repository.rs crates/zesdex-cms/src/infrastructure/persistence/rewind_blob_repo.rs crates/zesdex-cms/src/infrastructure/persistence/mod.rs crates/zesdex-cms/Cargo.toml
git commit -m "feat(cms): tambahkan RewindBlobRepository berbasis file (pengganti tabel blobs SQLite)"
Task 3: Rewire message archiving (archive_message) to Conversation
Files:
- Modify:
crates/zesdex-backend/src/app/runtime/actions/mod.rs(lines ~695-708spawn_turn, ~727-744TurnCtx, ~862-873archive_message, plus all 7 call sites at lines 939, 1105, 1295, 1387, 1421, 1426, 1458 — re-confirm line numbers first since Task 5 of the OAuth/session plan and Task 5 of the settings/appconfig/memory/editlog plan may have shifted this file)
Interfaces:
-
Consumes:
zesdex_cms::domain::conversation::{Conversation, ChatMessage}(Task 1),zesdex_cms::infrastructure::persistence::conversation_repo::JsonConversationRepository,zesdex_cms::domain::repository::ConversationRepository. -
Produces:
TurnCtx.conversation: Option<Arc<Mutex<Conversation>>>(replacesTurnCtx.db: Option<Arc<Mutex<rusqlite::Connection>>>). -
Step 1: Confirm current line numbers
Run: grep -n "fn spawn_turn\|struct TurnCtx\|fn archive_message\|open_or_create\|tc\.db\.as_ref" crates/zesdex-backend/src/app/runtime/actions/mod.rs
- Step 2: Replace the per-turn connection setup in
spawn_turn
Replace:
let db = crate::model::msglog::open_or_create(&edit_session_dir)
.ok()
.map(|c| std::sync::Arc::new(std::sync::Mutex::new(c)));
with:
let conversation = {
let repo = zesdex_cms::infrastructure::persistence::conversation_repo::JsonConversationRepository::new();
let conv = repo.load(&edit_session_dir).unwrap_or_else(|_| {
zesdex_cms::domain::conversation::Conversation::new(String::new(), session_id.clone())
});
Some(std::sync::Arc::new(std::sync::Mutex::new(conv)))
};
Replace the TurnCtx struct literal's db, field with conversation,.
- Step 3: Update the
TurnCtxstruct definition
Replace:
db: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
with:
conversation: Option<std::sync::Arc<std::sync::Mutex<zesdex_cms::domain::conversation::Conversation>>>,
- Step 4: Rewrite
archive_message
Replace:
fn archive_message(db: Option<&std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>, session_id: &str, msg: &ChatMessage) {
if let Some(arc) = db {
if let Ok(conn) = arc.lock() {
let _ = crate::model::msglog::insert_message(&conn, session_id, msg);
}
}
}
with:
/// Persist a `ChatMessage` to the session's `Conversation`, if one is
/// available for this turn.
///
/// Flow: if `conversation` is `Some`, lock the mutex, push the message,
/// and rewrite `conversation.json` in full. Errors are silently ignored
/// (matches the previous `SQLite`-backed behavior, which also swallowed
/// insert failures).
fn archive_message(
conversation: Option<&std::sync::Arc<std::sync::Mutex<zesdex_cms::domain::conversation::Conversation>>>,
session_dir: &std::path::Path,
msg: &ChatMessage,
) {
if let Some(arc) = conversation {
if let Ok(mut conv) = arc.lock() {
conv.push(msg.clone());
let repo = zesdex_cms::infrastructure::persistence::conversation_repo::JsonConversationRepository::new();
let _ = repo.save(session_dir, &conv);
}
}
}
- Step 5: Update all 7 call sites
Run: grep -n "archive_message(tc.db.as_ref()" crates/zesdex-backend/src/app/runtime/actions/mod.rs
At each of the 7 matched lines, replace archive_message(tc.db.as_ref(), &tc.session_id, &<msg_var>) with archive_message(tc.conversation.as_ref(), &tc.edit_log_session_dir, &<msg_var>) (keep whatever the actual message-variable name is at each site — sys, pipeline_msg, response, review_msg, tool_msg, msg per the research brief — only the first two arguments change).
- Step 6: Build
Run: cargo check -p zesdex-backend
Expected: no errors (beyond anything Task 4's blob work below still needs to touch in the same file — if this task is done independently, store_blob call sites will still fail to compile at this point; that's expected and resolved by Task 4).
- Step 7: Commit
git add crates/zesdex-backend/src/app/runtime/actions/mod.rs
git commit -m "refactor(backend): alihkan archive_message dari SQLite messages table ke zesdex-cms Conversation"
Task 4: Rewire rewind blob storage to FileRewindBlobRepository
Files:
- Modify:
crates/zesdex-backend/src/app/subagent/engine.rs(2store_blobcall sites, confirmed at line 531-533 and a second one — re-grep to find both) - Modify:
crates/zesdex-backend/src/app/mode/rewind.rs(rewind_count,rewind_to, plus theopen_session_dbhelper and the still-existing store_blob call site insideexecute_one_tool/wherever the second engine.rs call lives)
Interfaces:
-
Consumes:
zesdex_cms::infrastructure::persistence::rewind_blob_repo::FileRewindBlobRepository,zesdex_cms::domain::repository::RewindBlobRepository. -
Produces: nothing new for other tasks — this is the last consumer of the old
msglog::blobsmodule. -
Step 1: Find both
store_blobcall sites inengine.rs
Run: grep -n -B6 "store_blob" crates/zesdex-backend/src/app/subagent/engine.rs
- Step 2: Replace each
store_blobcall site
Replace (pattern applies to both sites, adjusting the surrounding variable names per the actual code read in Step 1):
let _ = crate::model::msglog::store_blob(
&conn, session_id, &tool_call.id, &bytes, None,
);
with:
let _ = zesdex_cms::infrastructure::persistence::rewind_blob_repo::FileRewindBlobRepository::new()
.store_blob(&ctx.session_dir, &tool_call.id, &bytes, None);
(Uses RewindBlobRepository::store_blob — add use zesdex_cms::domain::repository::RewindBlobRepository; to the file's imports. Note this drops the now-unneeded conn/session_id-derived-from-directory-name dance since the new repository takes session_dir directly — if the surrounding code only opened conn for this call, remove the now-dead connection-opening code too after confirming via Step 1's grep that nothing else in the same scope still needs it.)
- Step 3: Rewrite
rewind.rs'sopen_session_dbusage
Read the whole file first: cat crates/zesdex-backend/src/app/mode/rewind.rs
Replace rewind_count:
pub fn rewind_count(state: &AppStateRest) -> usize {
let Ok(conn) = open_session_db(&state.session_dir) else {
return 0;
};
crate::model::msglog::blobs::list_blob_keys(&conn, &state.session_id)
.ok()
.map_or(0, |keys| keys.len())
}
with:
pub fn rewind_count(state: &AppStateRest) -> usize {
zesdex_cms::infrastructure::persistence::rewind_blob_repo::FileRewindBlobRepository::new()
.list_blob_keys(&state.session_dir)
.ok()
.map_or(0, |keys| keys.len())
}
Replace the body of rewind_to (the open_session_db call plus list_blob_keys/retrieve_blob calls):
let conn = match open_session_db(&state.session_dir) { /* ... */ };
let keys = match crate::model::msglog::blobs::list_blob_keys(&conn, &state.session_id) { /* ... */ };
/* ... */
let bytes = match crate::model::msglog::blobs::retrieve_blob(&conn, &state.session_id, blob_key) { /* ... */ };
with (dropping the conn/open_session_db step entirely — the file-based repository needs no connection object, just &state.session_dir):
let repo = zesdex_cms::infrastructure::persistence::rewind_blob_repo::FileRewindBlobRepository::new();
let keys = match repo.list_blob_keys(&state.session_dir) {
Ok(k) => k,
Err(e) => {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
format!("Failed to list snapshots: {e}"),
));
state.dirty = true;
return;
}
};
if keys.is_empty() || index >= keys.len() {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Warning,
"No snapshot available at that index".to_string(),
));
state.dirty = true;
return;
}
let blob_key = &keys[index];
let bytes = match repo.retrieve_blob(&state.session_dir, blob_key) {
Ok(Some(b)) => b,
Ok(None) => {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
"Snapshot data not found".to_string(),
));
state.dirty = true;
return;
}
Err(e) => {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
format!("Failed to retrieve snapshot: {e}"),
));
state.dirty = true;
return;
}
};
(Keep whatever code follows bytes unchanged — the actual file-restoration logic doesn't depend on how bytes was fetched.)
- Step 4: Delete the now-unused
open_session_dbhelper
If open_session_db (used only by the two call sites just replaced) has no other callers after Step 3 — verify with grep -n "open_session_db" crates/zesdex-backend/src/app/mode/rewind.rs — delete its definition entirely.
- Step 5: Update the module doc comment
Replace the file's top doc comment:
//! Rewind mode: restores a file to a pre-edit snapshot stored in the
//! session's `SQLite` blob store.
with:
//! Rewind mode: restores a file to a pre-edit snapshot stored in the
//! session's file-based rewind blob store (`<session_dir>/blobs/`).
- Step 6: Build
Run: cargo check -p zesdex-backend
Expected: no errors.
- Step 7: Run the workspace test suite
Run: cargo test --workspace
Expected: all pass.
- Step 8: Manual smoke test
In the TUI: perform a file edit (triggers a pre-edit snapshot store), open the Rewind overlay, confirm the snapshot count and list are correct, and restore the file — confirm the restored content matches the pre-edit version exactly.
- Step 9: Commit
git add crates/zesdex-backend/src/app/subagent/engine.rs crates/zesdex-backend/src/app/mode/rewind.rs
git commit -m "refactor(backend): alihkan penyimpanan blob rewind ke FileRewindBlobRepository"
Task 5: Delete the now-dead msglog SQLite module
Files:
- Delete:
crates/zesdex-backend/src/model/msglog/{mod.rs,schema.rs,query.rs,blobs.rs} - Modify:
crates/zesdex-backend/src/model/mod.rs(removepub mod msglog;) - Modify:
crates/zesdex-backend/src/main.rs(line ~236,edit_count: state.edit_log.len() as u32,— confirm this doesn't referencemsglogdirectly; if it only readsstate.edit_log, no change needed here)
Interfaces: none — pure deletion after Tasks 3-4 remove every reference.
- Step 1: Verify zero remaining references
Run: grep -rln "model::msglog\|msglog::" crates/zesdex-backend/src
Expected: no output (only the model/mod.rs declaration itself, addressed in Step 3).
- Step 2: Delete the files
git rm -r crates/zesdex-backend/src/model/msglog
- Step 3: Remove the module declaration
In crates/zesdex-backend/src/model/mod.rs, remove:
pub mod msglog;
- Step 4: Build the whole workspace
Run: cargo build --workspace
Expected: no errors. If rusqlite was only pulled into zesdex-backend for this module, cargo build will still succeed since rusqlite remains a workspace dependency used elsewhere (zesdex-libs::database.rs) — no Cargo.toml change needed here; confirm with grep -rln "rusqlite" crates/zesdex-backend/src that no other file in this crate still needs it, and if truly zero remaining uses, remove rusqlite from crates/zesdex-backend/Cargo.toml's [dependencies] as a final cleanup (only if the grep comes back empty).
- Step 5: Run the full test suite and clippy
Run: cargo test --workspace && cargo clippy --workspace --all-targets -- -D warnings
Expected: all pass, no new warnings.
- Step 6: Commit
git add -A
git commit -m "chore: hapus modul msglog SQLite lama (digantikan Conversation + RewindBlobRepository)"