1use super::args::RememberArgs;
4use super::graph_input::normalize_and_validate_graph_input;
5use crate::chunking;
6use crate::entity_type::DEFAULT_ENTITY_TYPE;
7use crate::errors::AppError;
8use crate::i18n::errors_msg;
9use crate::output;
10use crate::paths::AppPaths;
11use crate::storage::chunks as storage_chunks;
12use crate::storage::connection::{ensure_schema, open_rw};
13use crate::storage::entities::{self as entities, NewEntity};
14use crate::storage::memories::{self as memories, NewMemory};
15use crate::storage::versions;
16
17pub fn run(args: RememberArgs, backends: crate::cli::BackendChoice) -> Result<(), AppError> {
19 let crate::cli::BackendChoice {
20 llm: llm_backend,
21 embedding: embedding_backend,
22 } = backends;
23 use crate::constants::*;
24
25 let started = std::time::Instant::now();
26 let _ = args.format;
27 tracing::debug!(
28 target: "remember",
29 name = ?args.name_positional.as_deref().or(args.name.as_deref()),
30 "persisting memory"
31 );
32 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
33
34 let resolved_name = super::name::resolve(&args)?;
35 let original_name = resolved_name.original;
36 let normalized_name = resolved_name.normalized;
37 let name_was_normalized = resolved_name.was_normalized;
38
39 let resolved_input = super::input::resolve(&args)?;
40 let mut raw_body = resolved_input.raw_body;
41 let mut graph = resolved_input.graph;
42 let entities_provided_externally = resolved_input.entities_provided_externally;
43 let mut relationships_updated = resolved_input.relationships_updated;
44
45 if args.strict_entity_types && !graph.type_warnings.is_empty() {
51 return Err(AppError::Validation(
52 crate::i18n::validation::strict_entity_type_folded(&graph.type_warnings),
53 ));
54 }
55
56 crate::memory_guard::check_embedding_input_size(&raw_body)?;
61
62 let body_will_be_preserved = args.force_merge && raw_body.trim().is_empty() && !args.clear_body;
67 if !entities_provided_externally
68 && graph.entities.is_empty()
69 && raw_body.trim().is_empty()
70 && !body_will_be_preserved
71 && !args.clear_body
72 {
73 return Err(AppError::Validation(crate::i18n::validation::empty_body()));
74 }
75
76 let metadata: serde_json::Value = if let Some(ref m) = args.metadata {
77 serde_json::from_str(m)?
78 } else if let Some(ref path) = args.metadata_file {
79 let file_size = std::fs::metadata(path).map_err(AppError::Io)?.len();
80 if file_size > MAX_MEMORY_BODY_LEN as u64 {
81 return Err(AppError::BodyTooLarge {
82 bytes: file_size,
83 limit: MAX_MEMORY_BODY_LEN as u64,
84 });
85 }
86 let content = std::fs::read_to_string(path).map_err(AppError::Io)?;
87 serde_json::from_str(&content)?
88 } else {
89 serde_json::json!({})
90 };
91
92 let mut body_hash = blake3::hash(raw_body.as_bytes()).to_hex().to_string();
93 let mut snippet: String = raw_body.chars().take(200).collect();
94
95 let paths = AppPaths::resolve(args.db.as_deref())?;
96 paths.ensure_dirs()?;
97
98 let mut extraction_method: Option<String> = None;
100 let mut extracted_urls: Vec<crate::extraction::ExtractedUrl> = Vec::with_capacity(4);
101 if args.enable_ner && args.skip_extraction {
102 return Err(AppError::Validation(
103 crate::i18n::validation::enable_ner_skip_extraction_exclusive(),
104 ));
105 }
106 if args.skip_extraction && !args.enable_ner {
107 tracing::warn!(
114 "--skip-extraction is deprecated since v1.0.45 and has no effect (NER is disabled by default); remove this flag to silence the warning"
115 );
116 }
117 if args.enable_ner && graph.entities.is_empty() && !raw_body.trim().is_empty() {
118 match crate::extraction::extract_graph_auto(&raw_body, &paths) {
119 Ok(extracted) => {
120 extraction_method = Some("url-regex".to_string());
124 extracted_urls = extracted.urls;
125 graph.entities = extracted
128 .entities
129 .into_iter()
130 .map(|e| NewEntity {
131 name: e.name,
132 entity_type: DEFAULT_ENTITY_TYPE.to_string(),
133 description: None,
134 })
135 .collect();
136 graph.relationships.clear();
137 relationships_updated = false;
138
139 if graph.entities.len() > max_entities_per_memory() {
140 graph.entities.truncate(max_entities_per_memory());
141 }
142 if graph.relationships.len() > max_relationships_per_memory() {
143 relationships_updated = true;
144 graph.relationships.truncate(max_relationships_per_memory());
145 }
146 normalize_and_validate_graph_input(&mut graph)?;
147 }
148 Err(e) => {
149 tracing::warn!(target: "remember", error = %e, "auto-extraction failed, graceful degradation");
150 extraction_method = Some("none:extraction-failed".to_string());
151 }
152 }
153 }
154
155 let mut conn = open_rw(&paths.db)?;
156 ensure_schema(&mut conn)?;
157
158 if args.dry_run {
160 let existing = memories::find_by_name(&conn, &namespace, &normalized_name)?;
161 let planned_action = if existing.is_some() && args.force_merge {
162 "would_update"
163 } else {
164 "would_create"
165 };
166 output::emit_json(&serde_json::json!({
174 "dry_run": true,
175 "name": normalized_name,
176 "namespace": namespace,
177 "planned_action": planned_action,
178 "entities_parsed": graph.entities.len(),
179 "relationships_parsed": graph.relationships.len(),
180 "warnings": graph.type_warnings,
181 }))?;
182 return Ok(());
183 }
184
185 {
186 use crate::constants::MAX_NAMESPACES_ACTIVE;
187 let active_count: u32 = conn.query_row(
188 "SELECT COUNT(DISTINCT namespace) FROM memories WHERE deleted_at IS NULL",
189 [],
190 |r| r.get::<_, i64>(0).map(|v| v as u32),
191 )?;
192 let ns_exists: bool = conn.query_row(
193 "SELECT EXISTS(SELECT 1 FROM memories WHERE namespace = ?1 AND deleted_at IS NULL)",
194 rusqlite::params![namespace],
195 |r| r.get::<_, i64>(0).map(|v| v > 0),
196 )?;
197 if !ns_exists && active_count >= MAX_NAMESPACES_ACTIVE {
198 return Err(AppError::NamespaceError(
199 crate::i18n::errors_ops::active_namespace_limit_reached(
200 MAX_NAMESPACES_ACTIVE,
201 &namespace,
202 ),
203 ));
204 }
205 }
206
207 if let Some((sd_id, true)) =
209 memories::find_by_name_any_state(&conn, &namespace, &normalized_name)?
210 {
211 if args.force_merge {
212 memories::clear_deleted_at(&conn, sd_id)?;
213 } else {
214 return Err(AppError::Duplicate(
215 errors_msg::duplicate_memory_soft_deleted(&normalized_name, &namespace),
216 ));
217 }
218 }
219
220 let existing_memory = memories::find_by_name(&conn, &namespace, &normalized_name)?;
221 if existing_memory.is_some() && !args.force_merge {
222 return Err(AppError::Duplicate(errors_msg::duplicate_memory(
223 &normalized_name,
224 &namespace,
225 )));
226 }
227
228 let (resolved_type, resolved_description) = if existing_memory.is_none() {
232 let t = args.r#type.ok_or_else(|| {
234 AppError::Validation(crate::i18n::validation::type_and_description_required())
235 })?;
236 let d = args.description.clone().ok_or_else(|| {
237 AppError::Validation(crate::i18n::validation::type_and_description_required())
238 })?;
239 (t.as_str().to_string(), d)
240 } else {
241 let existing_row = memories::read_by_name(&conn, &namespace, &normalized_name)?
243 .ok_or_else(|| {
244 AppError::NotFound(
245 crate::i18n::validation::memory_named_not_found_in_namespace(
246 &normalized_name,
247 &namespace,
248 ),
249 )
250 })?;
251 let t = args
252 .r#type
253 .map(|v| v.as_str().to_string())
254 .unwrap_or_else(|| existing_row.memory_type.clone());
255 let d = args
256 .description
257 .clone()
258 .unwrap_or_else(|| existing_row.description.clone());
259 (t, d)
260 };
261
262 if body_will_be_preserved {
267 if let Some(existing_row) = memories::read_by_name(&conn, &namespace, &normalized_name)? {
268 if !existing_row.body.is_empty() {
269 tracing::debug!(target: "remember",
270 name = %normalized_name,
271 "GAP-08: empty body with --force-merge and no --clear-body; preserving existing body"
272 );
273 raw_body = existing_row.body;
274 body_hash = blake3::hash(raw_body.as_bytes()).to_hex().to_string();
275 snippet = raw_body.chars().take(200).collect();
276 }
277 }
278 }
279
280 let duplicate_hash_id = memories::find_by_hash(&conn, &namespace, &body_hash)?;
281
282 output::emit_progress_i18n(
283 &format!(
284 "Remember stage: validated input; available memory {} MB",
285 crate::memory_guard::available_memory_mb()
286 ),
287 &format!(
288 "Stage remember: input validated; available memory {} MB",
289 crate::memory_guard::available_memory_mb()
290 ),
291 );
292
293 let model_max_length = crate::tokenizer::get_model_max_length();
294 let total_passage_tokens = crate::tokenizer::count_passage_tokens(&raw_body)?;
295 let chunks_info = chunking::split_into_chunks_hierarchical(&raw_body);
296 let chunks_created = chunks_info.len();
297 output::emit_progress_i18n(
303 &format!(
304 "Remember stage: tokenizer counted {total_passage_tokens} passage tokens (model max {model_max_length}); chunking produced {} chunks; process RSS {} MB",
305 chunks_created,
306 crate::memory_guard::current_process_memory_mb().unwrap_or(0)
307 ),
308 &format!(
309 "Stage remember: tokenizer counted {total_passage_tokens} passage tokens (model max {model_max_length}); chunking produced {} chunks; process RSS {} MB",
310 chunks_created,
311 crate::memory_guard::current_process_memory_mb().unwrap_or(0)
312 ),
313 );
314
315 if chunks_created > crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNKS {
316 return Err(AppError::TooManyChunks {
317 chunks: chunks_created,
318 limit: crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNKS,
319 });
320 }
321
322 let embed_out = super::embed_phase::run_embed_phase(
323 &paths,
324 &raw_body,
325 &chunks_info,
326 &graph,
327 &args,
328 crate::cli::BackendChoice::new(llm_backend, embedding_backend),
329 )?;
330 let embedding = embed_out.embedding;
331 let backend_invoked_passage = embed_out.backend_invoked_passage;
332 let mut chunk_embeddings_cache = embed_out.chunk_embeddings_cache;
333 let graph_entity_embeddings = embed_out.graph_entity_embeddings;
334 let _skip_embed = embed_out.skip_embed;
335
336 let body_for_storage = raw_body;
337 let memory_type = resolved_type.as_str();
338 let new_memory = NewMemory {
339 namespace: namespace.clone(),
340 name: normalized_name.clone(),
341 memory_type: memory_type.to_string(),
342 description: resolved_description.clone(),
343 body: body_for_storage,
344 body_hash: body_hash.clone(),
345 session_id: args.session_id.clone(),
346 source: "agent".to_string(),
347 metadata,
348 };
349
350 let mut warnings = Vec::with_capacity(4);
351 warnings.append(&mut graph.type_warnings);
356 let mut entities_persisted = 0usize;
357 let mut relationships_persisted = 0usize;
358
359 let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
360
361 let mut skip_reindex = false;
362 let (memory_id, action, version) = match existing_memory {
363 Some((existing_id, _updated_at, _current_version)) => {
364 if let Some(hash_id) = duplicate_hash_id {
365 if hash_id != existing_id {
366 warnings.push(format!(
367 "identical body already exists as memory id {hash_id}"
368 ));
369 }
370 }
371
372 let (old_fts_name, old_fts_desc, old_fts_body): (String, String, String) = tx
374 .query_row(
375 "SELECT name, description, body FROM memories WHERE id = ?1",
376 rusqlite::params![existing_id],
377 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
378 )?;
379
380 let existing_body_hash: Option<String> = tx
382 .query_row(
383 "SELECT body_hash FROM memories WHERE id = ?1",
384 rusqlite::params![existing_id],
385 |r| r.get(0),
386 )
387 .ok();
388 let body_unchanged = existing_body_hash.as_deref() == Some(&body_hash);
389 skip_reindex = body_unchanged;
390 if !body_unchanged {
391 storage_chunks::delete_chunks(&tx, existing_id)?;
392 }
393
394 let next_v = versions::next_version(&tx, existing_id)?;
395 memories::update(&tx, existing_id, &new_memory, args.expected_updated_at)?;
396
397 memories::sync_fts_after_update(
400 &tx,
401 existing_id,
402 &old_fts_name,
403 &old_fts_desc,
404 &old_fts_body,
405 &normalized_name,
406 &resolved_description,
407 &new_memory.body,
408 )?;
409
410 versions::insert_version(
411 &tx,
412 existing_id,
413 next_v,
414 &normalized_name,
415 memory_type,
416 &resolved_description,
417 &new_memory.body,
418 &serde_json::to_string(&new_memory.metadata)?,
419 None,
420 "edit",
421 )?;
422 if !body_unchanged {
423 if let Some(ref emb) = embedding {
424 memories::upsert_vec(
425 &tx,
426 existing_id,
427 &namespace,
428 memory_type,
429 emb,
430 &normalized_name,
431 &snippet,
432 )?;
433 }
434 }
435 (existing_id, "updated".to_string(), next_v)
436 }
437 None => {
438 if let Some(hash_id) = duplicate_hash_id {
439 warnings.push(format!(
440 "identical body already exists as memory id {hash_id}"
441 ));
442 }
443 let id = memories::insert(&tx, &new_memory)?;
444 versions::insert_version(
445 &tx,
446 id,
447 1,
448 &normalized_name,
449 memory_type,
450 &resolved_description,
451 &new_memory.body,
452 &serde_json::to_string(&new_memory.metadata)?,
453 None,
454 "create",
455 )?;
456 if let Some(ref emb) = embedding {
457 memories::upsert_vec(
458 &tx,
459 id,
460 &namespace,
461 memory_type,
462 emb,
463 &normalized_name,
464 &snippet,
465 )?;
466 }
467 (id, "created".to_string(), 1)
468 }
469 };
470
471 if args.replace_graph && action == "updated" {
477 let (e_removed, r_removed) = entities::clear_memory_graph_bindings(&tx, memory_id)?;
478 if e_removed + r_removed > 0 {
479 warnings.push(format!(
480 "--replace-graph cleared {e_removed} entity binding(s) and {r_removed} relationship binding(s) before re-linking"
481 ));
482 }
483 }
484
485 if chunks_info.len() > 1 && !skip_reindex {
486 storage_chunks::insert_chunk_slices(&tx, memory_id, &new_memory.body, &chunks_info)?;
487
488 if let Some(chunk_embeddings) = chunk_embeddings_cache.take() {
489 for (i, emb) in chunk_embeddings.iter().enumerate() {
490 storage_chunks::upsert_chunk_vec(&tx, i as i64, memory_id, i as i32, emb)?;
491 }
492 }
493 output::emit_progress_i18n(
494 &format!(
495 "Remember stage: persisted chunk vectors; process RSS {} MB",
496 crate::memory_guard::current_process_memory_mb().unwrap_or(0)
497 ),
498 &format!(
499 "Etapa remember: vetores de chunks persistidos; RSS do processo {} MB",
500 crate::memory_guard::current_process_memory_mb().unwrap_or(0)
501 ),
502 );
503 }
504
505 if !graph.entities.is_empty() || !graph.relationships.is_empty() {
506 for entity in &graph.entities {
507 let entity_id = entities::upsert_entity(&tx, &namespace, entity)?;
508 let entity_embedding = &graph_entity_embeddings[entities_persisted];
509 entities::upsert_entity_vec(
510 &tx,
511 entity_id,
512 &namespace,
513 &entity.entity_type,
514 entity_embedding,
515 &entity.name,
516 )?;
517 entities::link_memory_entity(&tx, memory_id, entity_id)?;
518 entities_persisted += 1;
519 }
520 let entity_types: std::collections::HashMap<&str, &str> = graph
521 .entities
522 .iter()
523 .map(|entity| (entity.name.as_str(), entity.entity_type.as_str()))
524 .collect();
525
526 let mut affected_entity_ids: std::collections::HashSet<i64> =
527 std::collections::HashSet::new();
528 for entity in &graph.entities {
529 if let Some(eid) = entities::find_entity_id(&tx, &namespace, &entity.name)? {
530 affected_entity_ids.insert(eid);
531 }
532 }
533
534 for rel in &graph.relationships {
535 let source_entity = NewEntity {
536 name: rel.source.clone(),
537 entity_type: entity_types
538 .get(rel.source.as_str())
539 .copied()
540 .unwrap_or(DEFAULT_ENTITY_TYPE)
541 .to_string(),
542 description: None,
543 };
544 let target_entity = NewEntity {
545 name: rel.target.clone(),
546 entity_type: entity_types
547 .get(rel.target.as_str())
548 .copied()
549 .unwrap_or(DEFAULT_ENTITY_TYPE)
550 .to_string(),
551 description: None,
552 };
553 let source_id = entities::upsert_entity(&tx, &namespace, &source_entity)?;
554 let target_id = entities::upsert_entity(&tx, &namespace, &target_entity)?;
555 let rel_id = entities::upsert_relationship(&tx, &namespace, source_id, target_id, rel)?;
556 entities::link_memory_relationship(&tx, memory_id, rel_id)?;
557 affected_entity_ids.insert(source_id);
558 affected_entity_ids.insert(target_id);
559 relationships_persisted += 1;
560 }
561
562 for &eid in &affected_entity_ids {
563 entities::recalculate_degree(&tx, eid)?;
564 }
565 }
566 tx.commit()?;
567
568 super::finish::emit_remember_result(
569 super::finish::FinishContext {
570 conn: &conn,
571 paths: &paths,
572 args: &args,
573 graph: &graph,
574 new_memory: &new_memory,
575 },
576 super::finish::FinishIdentity {
577 memory_id,
578 namespace,
579 normalized_name,
580 original_name,
581 name_was_normalized,
582 },
583 super::finish::FinishOutcome {
584 action,
585 version,
586 entities_persisted,
587 relationships_persisted,
588 relationships_updated,
589 chunks_created,
590 },
591 super::finish::FinishExtraction {
592 extracted_urls,
593 extraction_method,
594 backend_invoked_passage,
595 },
596 warnings,
597 started,
598 )?;
599
600 Ok(())
601}