Skip to main content

sqlite_graphrag/commands/
remember.rs

1use crate::chunking;
2use crate::cli::MemoryType;
3use crate::errors::AppError;
4use crate::i18n::erros;
5use crate::output::{self, JsonOutputFormat, RememberResponse};
6use crate::paths::AppPaths;
7use crate::storage::chunks as storage_chunks;
8use crate::storage::connection::open_rw;
9use crate::storage::entities::{NewEntity, NewRelationship};
10use crate::storage::memories::NewMemory;
11use crate::storage::{entities, memories, versions};
12use serde::Deserialize;
13use std::io::Read as _;
14
15#[derive(clap::Args)]
16pub struct RememberArgs {
17    #[arg(long)]
18    pub name: String,
19    #[arg(
20        long,
21        value_enum,
22        long_help = "Memory kind stored in `memories.type`. This is NOT the graph `entity_type` used in `--entities-file`. Valid values: user, feedback, project, reference, decision, incident, skill."
23    )]
24    pub r#type: MemoryType,
25    #[arg(long)]
26    pub description: String,
27    #[arg(
28        long,
29        conflicts_with_all = ["body_file", "body_stdin", "graph_stdin"]
30    )]
31    pub body: Option<String>,
32    #[arg(
33        long,
34        conflicts_with_all = ["body", "body_stdin", "graph_stdin"]
35    )]
36    pub body_file: Option<std::path::PathBuf>,
37    #[arg(
38        long,
39        conflicts_with_all = ["body", "body_file", "graph_stdin"]
40    )]
41    pub body_stdin: bool,
42    #[arg(long)]
43    pub entities_file: Option<std::path::PathBuf>,
44    #[arg(long)]
45    pub relationships_file: Option<std::path::PathBuf>,
46    #[arg(
47        long,
48        conflicts_with_all = [
49            "body",
50            "body_file",
51            "body_stdin",
52            "entities_file",
53            "relationships_file"
54        ]
55    )]
56    pub graph_stdin: bool,
57    #[arg(long, default_value = "global")]
58    pub namespace: Option<String>,
59    #[arg(long)]
60    pub metadata: Option<String>,
61    #[arg(long)]
62    pub metadata_file: Option<std::path::PathBuf>,
63    #[arg(long)]
64    pub force_merge: bool,
65    #[arg(
66        long,
67        value_name = "EPOCH_OR_RFC3339",
68        value_parser = crate::parsers::parse_expected_updated_at,
69        long_help = "Optimistic lock: reject if updated_at does not match. \
70Accepts Unix epoch (e.g. 1700000000) or RFC 3339 (e.g. 2026-04-19T12:00:00Z)."
71    )]
72    pub expected_updated_at: Option<i64>,
73    #[arg(long)]
74    pub skip_extraction: bool,
75    #[arg(long)]
76    pub session_id: Option<String>,
77    #[arg(long, value_enum, default_value_t = JsonOutputFormat::Json)]
78    pub format: JsonOutputFormat,
79    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
80    pub json: bool,
81    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
82    pub db: Option<String>,
83}
84
85#[derive(Deserialize, Default)]
86struct GraphInput {
87    #[serde(default)]
88    entities: Vec<NewEntity>,
89    #[serde(default)]
90    relationships: Vec<NewRelationship>,
91}
92
93pub fn run(args: RememberArgs) -> Result<(), AppError> {
94    use crate::constants::*;
95
96    let inicio = std::time::Instant::now();
97    let _ = args.format;
98    let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
99
100    if args.name.is_empty() || args.name.len() > MAX_MEMORY_NAME_LEN {
101        return Err(AppError::Validation(
102            crate::i18n::validacao::nome_comprimento(MAX_MEMORY_NAME_LEN),
103        ));
104    }
105
106    if args.name.starts_with("__") {
107        return Err(AppError::Validation(
108            crate::i18n::validacao::nome_reservado(),
109        ));
110    }
111
112    {
113        let slug_re = regex::Regex::new(crate::constants::NAME_SLUG_REGEX)
114            .map_err(|e| AppError::Internal(anyhow::anyhow!("regex: {e}")))?;
115        if !slug_re.is_match(&args.name) {
116            return Err(AppError::Validation(crate::i18n::validacao::nome_kebab(
117                &args.name,
118            )));
119        }
120    }
121
122    if args.description.len() > MAX_MEMORY_DESCRIPTION_LEN {
123        return Err(AppError::Validation(
124            crate::i18n::validacao::descricao_excede(MAX_MEMORY_DESCRIPTION_LEN),
125        ));
126    }
127
128    let mut raw_body = if let Some(b) = args.body {
129        if b.len() > REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES {
130            return Err(AppError::LimitExceeded(format!(
131                "documento tem {} bytes; limite operacional seguro atual é {REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES} bytes; reduza ou divida o documento antes de usar remember",
132                b.len()
133            )));
134        }
135        b
136    } else if let Some(path) = args.body_file {
137        let file_len = std::fs::metadata(&path).map_err(AppError::Io)?.len() as usize;
138        if file_len > REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES {
139            return Err(AppError::LimitExceeded(format!(
140                "arquivo tem {file_len} bytes; limite operacional seguro atual é {REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES} bytes; reduza ou divida o documento antes de usar remember"
141            )));
142        }
143        std::fs::read_to_string(&path).map_err(AppError::Io)?
144    } else if args.body_stdin || args.graph_stdin {
145        let mut buf = String::new();
146        std::io::stdin()
147            .read_to_string(&mut buf)
148            .map_err(AppError::Io)?;
149        if buf.len() > REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES {
150            return Err(AppError::LimitExceeded(format!(
151                "entrada stdin tem {} bytes; limite operacional seguro atual é {REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES} bytes; reduza ou divida o documento antes de usar remember",
152                buf.len()
153            )));
154        }
155        buf
156    } else {
157        String::new()
158    };
159
160    let mut graph = GraphInput::default();
161    if !args.skip_extraction {
162        if let Some(path) = args.entities_file {
163            let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
164            graph.entities = serde_json::from_str(&content)?;
165        }
166        if let Some(path) = args.relationships_file {
167            let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
168            graph.relationships = serde_json::from_str(&content)?;
169        }
170        if args.graph_stdin {
171            graph = serde_json::from_str::<GraphInput>(&raw_body).map_err(|e| {
172                AppError::Validation(format!("invalid --graph-stdin JSON payload: {e}"))
173            })?;
174            raw_body = String::new();
175        }
176    }
177
178    if graph.entities.len() > MAX_ENTITIES_PER_MEMORY {
179        return Err(AppError::LimitExceeded(erros::limite_entidades(
180            MAX_ENTITIES_PER_MEMORY,
181        )));
182    }
183    if graph.relationships.len() > MAX_RELATIONSHIPS_PER_MEMORY {
184        return Err(AppError::LimitExceeded(erros::limite_relacionamentos(
185            MAX_RELATIONSHIPS_PER_MEMORY,
186        )));
187    }
188
189    if raw_body.len() > MAX_MEMORY_BODY_LEN {
190        return Err(AppError::LimitExceeded(
191            crate::i18n::validacao::body_excede(MAX_MEMORY_BODY_LEN),
192        ));
193    }
194
195    let metadata: serde_json::Value = if let Some(m) = args.metadata {
196        serde_json::from_str(&m)?
197    } else if let Some(path) = args.metadata_file {
198        let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
199        serde_json::from_str(&content)?
200    } else {
201        serde_json::json!({})
202    };
203
204    let body_hash = blake3::hash(raw_body.as_bytes()).to_hex().to_string();
205    let snippet: String = raw_body.chars().take(200).collect();
206
207    let paths = AppPaths::resolve(args.db.as_deref())?;
208    let mut conn = open_rw(&paths.db)?;
209
210    {
211        use crate::constants::MAX_NAMESPACES_ACTIVE;
212        let active_count: u32 = conn.query_row(
213            "SELECT COUNT(DISTINCT namespace) FROM memories WHERE deleted_at IS NULL",
214            [],
215            |r| r.get::<_, i64>(0).map(|v| v as u32),
216        )?;
217        let ns_exists: bool = conn.query_row(
218            "SELECT EXISTS(SELECT 1 FROM memories WHERE namespace = ?1 AND deleted_at IS NULL)",
219            rusqlite::params![namespace],
220            |r| r.get::<_, i64>(0).map(|v| v > 0),
221        )?;
222        if !ns_exists && active_count >= MAX_NAMESPACES_ACTIVE {
223            return Err(AppError::NamespaceError(format!(
224                "limite de {MAX_NAMESPACES_ACTIVE} namespaces ativos excedido ao tentar criar '{namespace}'"
225            )));
226        }
227    }
228
229    let existing_memory = memories::find_by_name(&conn, &namespace, &args.name)?;
230    if existing_memory.is_some() && !args.force_merge {
231        return Err(AppError::Duplicate(erros::memoria_duplicada(
232            &args.name, &namespace,
233        )));
234    }
235
236    let duplicate_hash_id = memories::find_by_hash(&conn, &namespace, &body_hash)?;
237
238    output::emit_progress_i18n(
239        &format!(
240            "Remember stage: validated input; available memory {} MB",
241            crate::memory_guard::available_memory_mb()
242        ),
243        &format!(
244            "Etapa remember: entrada validada; memória disponível {} MB",
245            crate::memory_guard::available_memory_mb()
246        ),
247    );
248
249    let tokenizer = crate::tokenizer::get_tokenizer(&paths.models)?;
250    let model_max_length = crate::tokenizer::get_model_max_length(&paths.models)?;
251    let total_passage_tokens = crate::tokenizer::count_passage_tokens(tokenizer, &raw_body)?;
252    let token_offsets = crate::tokenizer::passage_token_offsets(tokenizer, &raw_body)?;
253    let chunks_info = chunking::split_into_chunks_by_token_offsets(&raw_body, &token_offsets);
254    let chunks_created = chunks_info.len();
255
256    output::emit_progress_i18n(
257        &format!(
258            "Remember stage: tokenizer counted {total_passage_tokens} passage tokens (model max {model_max_length}); chunking produced {} chunks; process RSS {} MB",
259            chunks_created,
260            crate::memory_guard::current_process_memory_mb().unwrap_or(0)
261        ),
262        &format!(
263            "Etapa remember: tokenizer contou {total_passage_tokens} tokens de passagem (máximo do modelo {model_max_length}); chunking gerou {} chunks; RSS do processo {} MB",
264            chunks_created,
265            crate::memory_guard::current_process_memory_mb().unwrap_or(0)
266        ),
267    );
268
269    if chunks_created > crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNKS {
270        return Err(AppError::LimitExceeded(format!(
271            "documento gera {chunks_created} chunks; limite operacional seguro atual é {} chunks; divida o documento antes de usar remember",
272            crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNKS
273        )));
274    }
275
276    if chunks_created > 1
277        && raw_body.len() > crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES
278    {
279        return Err(AppError::LimitExceeded(format!(
280            "documento multi-chunk tem {} bytes; limite operacional seguro atual é {} bytes; reduza ou divida o documento antes de usar remember",
281            raw_body.len(),
282            crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNK_BODY_BYTES
283        )));
284    }
285
286    output::emit_progress_i18n("Computing embedding...", "Calculando embedding...");
287    let mut chunk_embeddings_cache: Option<Vec<Vec<f32>>> = None;
288
289    let embedding = if chunks_info.len() == 1 {
290        crate::daemon::embed_passage_or_local(&paths.models, &raw_body)?
291    } else {
292        let chunk_texts: Vec<&str> = chunks_info
293            .iter()
294            .map(|c| chunking::chunk_text(&raw_body, c))
295            .collect();
296        let chunk_token_counts: Vec<usize> = chunk_texts
297            .iter()
298            .map(|text| crate::tokenizer::count_passage_tokens(tokenizer, text))
299            .collect::<Result<_, _>>()?;
300        let controlled_batches = crate::embedder::controlled_batch_count(&chunk_token_counts);
301        output::emit_progress_i18n(
302            &format!(
303                "Embedding {} chunks across {} controlled batches...",
304                chunks_info.len(),
305                controlled_batches
306            ),
307            &format!(
308                "Embedando {} chunks em {} batches controlados...",
309                chunks_info.len(),
310                controlled_batches
311            ),
312        );
313        let chunk_embeddings = crate::daemon::embed_passages_controlled_or_local(
314            &paths.models,
315            &chunk_texts,
316            &chunk_token_counts,
317        )?;
318        output::emit_progress_i18n(
319            &format!(
320                "Remember stage: chunk embeddings complete; process RSS {} MB",
321                crate::memory_guard::current_process_memory_mb().unwrap_or(0)
322            ),
323            &format!(
324                "Etapa remember: embeddings dos chunks concluídos; RSS do processo {} MB",
325                crate::memory_guard::current_process_memory_mb().unwrap_or(0)
326            ),
327        );
328        let aggregated = chunking::aggregate_embeddings(&chunk_embeddings);
329        chunk_embeddings_cache = Some(chunk_embeddings);
330        aggregated
331    };
332    let body_for_storage = raw_body;
333
334    let memory_type = args.r#type.as_str();
335    let new_memory = NewMemory {
336        namespace: namespace.clone(),
337        name: args.name.clone(),
338        memory_type: memory_type.to_string(),
339        description: args.description.clone(),
340        body: body_for_storage,
341        body_hash: body_hash.clone(),
342        session_id: args.session_id.clone(),
343        source: "agent".to_string(),
344        metadata,
345    };
346
347    let mut warnings = Vec::new();
348
349    let (memory_id, action, version) = match existing_memory {
350        Some((existing_id, _updated_at, _current_version)) => {
351            if let Some(hash_id) = duplicate_hash_id {
352                if hash_id != existing_id {
353                    warnings.push(format!(
354                        "identical body already exists as memory id {hash_id}"
355                    ));
356                }
357            }
358            let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
359
360            if chunks_info.len() > 1 {
361                storage_chunks::delete_chunks(&tx, existing_id)?;
362            }
363
364            let next_v = versions::next_version(&tx, existing_id)?;
365            memories::update(&tx, existing_id, &new_memory, args.expected_updated_at)?;
366            versions::insert_version(
367                &tx,
368                existing_id,
369                next_v,
370                &args.name,
371                memory_type,
372                &args.description,
373                &new_memory.body,
374                &serde_json::to_string(&new_memory.metadata)?,
375                None,
376                "edit",
377            )?;
378            memories::upsert_vec(
379                &tx,
380                existing_id,
381                &namespace,
382                memory_type,
383                &embedding,
384                &args.name,
385                &snippet,
386            )?;
387            tx.commit()?;
388            (existing_id, "updated".to_string(), next_v)
389        }
390        None => {
391            if let Some(hash_id) = duplicate_hash_id {
392                warnings.push(format!(
393                    "identical body already exists as memory id {hash_id}"
394                ));
395            }
396            let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
397            let id = memories::insert(&tx, &new_memory)?;
398            versions::insert_version(
399                &tx,
400                id,
401                1,
402                &args.name,
403                memory_type,
404                &args.description,
405                &new_memory.body,
406                &serde_json::to_string(&new_memory.metadata)?,
407                None,
408                "create",
409            )?;
410            memories::upsert_vec(
411                &tx,
412                id,
413                &namespace,
414                memory_type,
415                &embedding,
416                &args.name,
417                &snippet,
418            )?;
419            tx.commit()?;
420            (id, "created".to_string(), 1)
421        }
422    };
423
424    if chunks_info.len() > 1 {
425        let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
426        storage_chunks::insert_chunk_slices(&tx, memory_id, &new_memory.body, &chunks_info)?;
427
428        let chunk_embeddings = chunk_embeddings_cache.take().ok_or_else(|| {
429            AppError::Internal(anyhow::anyhow!(
430                "chunk embeddings cache missing for multi-chunk remember path"
431            ))
432        })?;
433
434        for (i, emb) in chunk_embeddings.iter().enumerate() {
435            storage_chunks::upsert_chunk_vec(&tx, i as i64, memory_id, i as i32, emb)?;
436        }
437        tx.commit()?;
438        output::emit_progress_i18n(
439            &format!(
440                "Remember stage: persisted chunk vectors; process RSS {} MB",
441                crate::memory_guard::current_process_memory_mb().unwrap_or(0)
442            ),
443            &format!(
444                "Etapa remember: vetores de chunks persistidos; RSS do processo {} MB",
445                crate::memory_guard::current_process_memory_mb().unwrap_or(0)
446            ),
447        );
448    }
449
450    let mut entities_persisted = 0usize;
451    let mut relationships_persisted = 0usize;
452
453    if !graph.entities.is_empty() || !graph.relationships.is_empty() {
454        let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
455        for entity in &graph.entities {
456            let entity_id = entities::upsert_entity(&tx, &namespace, entity)?;
457            let entity_text = match &entity.description {
458                Some(desc) => format!("{} {}", entity.name, desc),
459                None => entity.name.clone(),
460            };
461            let entity_embedding =
462                crate::daemon::embed_passage_or_local(&paths.models, &entity_text)?;
463            entities::upsert_entity_vec(
464                &tx,
465                entity_id,
466                &namespace,
467                &entity.entity_type,
468                &entity_embedding,
469                &entity.name,
470            )?;
471            entities::link_memory_entity(&tx, memory_id, entity_id)?;
472            entities::increment_degree(&tx, entity_id)?;
473            entities_persisted += 1;
474        }
475        let entity_types: std::collections::HashMap<&str, &str> = graph
476            .entities
477            .iter()
478            .map(|entity| (entity.name.as_str(), entity.entity_type.as_str()))
479            .collect();
480
481        for rel in &graph.relationships {
482            let source_entity = NewEntity {
483                name: rel.source.clone(),
484                entity_type: entity_types
485                    .get(rel.source.as_str())
486                    .copied()
487                    .unwrap_or("concept")
488                    .to_string(),
489                description: None,
490            };
491            let target_entity = NewEntity {
492                name: rel.target.clone(),
493                entity_type: entity_types
494                    .get(rel.target.as_str())
495                    .copied()
496                    .unwrap_or("concept")
497                    .to_string(),
498                description: None,
499            };
500            let source_id = entities::upsert_entity(&tx, &namespace, &source_entity)?;
501            let target_id = entities::upsert_entity(&tx, &namespace, &target_entity)?;
502            let rel_id = entities::upsert_relationship(&tx, &namespace, source_id, target_id, rel)?;
503            entities::link_memory_relationship(&tx, memory_id, rel_id)?;
504            relationships_persisted += 1;
505        }
506        tx.commit()?;
507    }
508
509    let created_at_epoch = chrono::Utc::now().timestamp();
510    let created_at_iso = crate::tz::formatar_iso(chrono::Utc::now());
511
512    output::emit_json(&RememberResponse {
513        memory_id,
514        name: args.name,
515        namespace,
516        action: action.clone(),
517        operation: action,
518        version,
519        entities_persisted,
520        relationships_persisted,
521        chunks_created,
522        merged_into_memory_id: None,
523        warnings,
524        created_at: created_at_epoch,
525        created_at_iso,
526        elapsed_ms: inicio.elapsed().as_millis() as u64,
527    })?;
528
529    Ok(())
530}
531
532#[cfg(test)]
533mod testes {
534    use crate::output::RememberResponse;
535
536    #[test]
537    fn remember_response_serializa_campos_obrigatorios() {
538        let resp = RememberResponse {
539            memory_id: 42,
540            name: "minha-mem".to_string(),
541            namespace: "global".to_string(),
542            action: "created".to_string(),
543            operation: "created".to_string(),
544            version: 1,
545            entities_persisted: 0,
546            relationships_persisted: 0,
547            chunks_created: 1,
548            merged_into_memory_id: None,
549            warnings: vec![],
550            created_at: 1_705_320_000,
551            created_at_iso: "2024-01-15T12:00:00Z".to_string(),
552            elapsed_ms: 55,
553        };
554
555        let json = serde_json::to_value(&resp).expect("serialização falhou");
556        assert_eq!(json["memory_id"], 42);
557        assert_eq!(json["action"], "created");
558        assert_eq!(json["operation"], "created");
559        assert_eq!(json["version"], 1);
560        assert_eq!(json["elapsed_ms"], 55u64);
561        assert!(json["warnings"].is_array());
562        assert!(json["merged_into_memory_id"].is_null());
563    }
564
565    #[test]
566    fn remember_response_action_e_operation_sao_aliases() {
567        let resp = RememberResponse {
568            memory_id: 1,
569            name: "mem".to_string(),
570            namespace: "global".to_string(),
571            action: "updated".to_string(),
572            operation: "updated".to_string(),
573            version: 2,
574            entities_persisted: 3,
575            relationships_persisted: 1,
576            chunks_created: 2,
577            merged_into_memory_id: None,
578            warnings: vec![],
579            created_at: 0,
580            created_at_iso: "1970-01-01T00:00:00Z".to_string(),
581            elapsed_ms: 0,
582        };
583
584        let json = serde_json::to_value(&resp).expect("serialização falhou");
585        assert_eq!(
586            json["action"], json["operation"],
587            "action e operation devem ser iguais"
588        );
589        assert_eq!(json["entities_persisted"], 3);
590        assert_eq!(json["relationships_persisted"], 1);
591        assert_eq!(json["chunks_created"], 2);
592    }
593
594    #[test]
595    fn remember_response_warnings_lista_mensagens() {
596        let resp = RememberResponse {
597            memory_id: 5,
598            name: "dup-mem".to_string(),
599            namespace: "global".to_string(),
600            action: "created".to_string(),
601            operation: "created".to_string(),
602            version: 1,
603            entities_persisted: 0,
604            relationships_persisted: 0,
605            chunks_created: 1,
606            merged_into_memory_id: None,
607            warnings: vec!["identical body already exists as memory id 3".to_string()],
608            created_at: 0,
609            created_at_iso: "1970-01-01T00:00:00Z".to_string(),
610            elapsed_ms: 10,
611        };
612
613        let json = serde_json::to_value(&resp).expect("serialização falhou");
614        let warnings = json["warnings"]
615            .as_array()
616            .expect("warnings deve ser array");
617        assert_eq!(warnings.len(), 1);
618        assert!(warnings[0].as_str().unwrap().contains("identical body"));
619    }
620
621    #[test]
622    fn nome_invalido_prefixo_reservado_retorna_validation_error() {
623        use crate::errors::AppError;
624        // Valida a lógica de rejeição de nomes com prefixo "__" diretamente
625        let nome = "__reservado";
626        let resultado: Result<(), AppError> = if nome.starts_with("__") {
627            Err(AppError::Validation(
628                crate::i18n::validacao::nome_reservado(),
629            ))
630        } else {
631            Ok(())
632        };
633        assert!(resultado.is_err());
634        if let Err(AppError::Validation(msg)) = resultado {
635            assert!(!msg.is_empty());
636        }
637    }
638
639    #[test]
640    fn nome_muito_longo_retorna_validation_error() {
641        use crate::errors::AppError;
642        let nome_longo = "a".repeat(crate::constants::MAX_MEMORY_NAME_LEN + 1);
643        let resultado: Result<(), AppError> =
644            if nome_longo.is_empty() || nome_longo.len() > crate::constants::MAX_MEMORY_NAME_LEN {
645                Err(AppError::Validation(
646                    crate::i18n::validacao::nome_comprimento(crate::constants::MAX_MEMORY_NAME_LEN),
647                ))
648            } else {
649                Ok(())
650            };
651        assert!(resultado.is_err());
652    }
653
654    #[test]
655    fn remember_response_merged_into_memory_id_some_serializa_inteiro() {
656        let resp = RememberResponse {
657            memory_id: 10,
658            name: "mem-mergeada".to_string(),
659            namespace: "global".to_string(),
660            action: "updated".to_string(),
661            operation: "updated".to_string(),
662            version: 3,
663            entities_persisted: 0,
664            relationships_persisted: 0,
665            chunks_created: 1,
666            merged_into_memory_id: Some(7),
667            warnings: vec![],
668            created_at: 0,
669            created_at_iso: "1970-01-01T00:00:00Z".to_string(),
670            elapsed_ms: 0,
671        };
672
673        let json = serde_json::to_value(&resp).expect("serialização falhou");
674        assert_eq!(json["merged_into_memory_id"], 7);
675    }
676}