Class: HTM

Inherits:
Object
  • Object
show all
Defined in:
lib/htm.rb,
lib/htm/config.rb,
lib/htm/config.rb,
lib/htm/errors.rb,
lib/htm/mcp/cli.rb,
lib/htm/railtie.rb,
lib/htm/version.rb,
lib/htm/database.rb,
lib/htm/mcp/tools.rb,
lib/htm/migration.rb,
lib/htm/telemetry.rb,
lib/htm/timeframe.rb,
lib/htm/mcp/server.rb,
lib/htm/models/tag.rb,
lib/htm/job_adapter.rb,
lib/htm/models/node.rb,
lib/htm/query_cache.rb,
lib/htm/robot_group.rb,
lib/htm/sql_builder.rb,
lib/htm/tag_service.rb,
lib/htm/models/robot.rb,
lib/htm/mcp/resources.rb,
lib/htm/observability.rb,
lib/htm/sequel_config.rb,
lib/htm/config/builder.rb,
lib/htm/working_memory.rb,
lib/htm/circuit_breaker.rb,
lib/htm/config/database.rb,
lib/htm/mcp/group_tools.rb,
lib/htm/models/node_tag.rb,
lib/htm/config/validator.rb,
lib/htm/long_term_memory.rb,
lib/htm/embedding_service.rb,
lib/htm/models/robot_node.rb,
lib/htm/models/file_source.rb,
lib/htm/proposition_service.rb,
lib/htm/timeframe_extractor.rb,
lib/htm/integrations/sinatra.rb,
lib/htm/jobs/generate_tags_job.rb,
lib/htm/working_memory_channel.rb,
lib/htm/loaders/markdown_loader.rb,
lib/htm/loaders/markdown_chunker.rb,
lib/htm/models/node_relationship.rb,
lib/htm/jobs/generate_embedding_job.rb,
lib/htm/workflows/remember_workflow.rb,
lib/htm/jobs/generate_propositions_job.rb,
lib/htm/long_term_memory/hybrid_search.rb,
lib/htm/long_term_memory/vector_search.rb,
lib/htm/jobs/generate_relationships_job.rb,
lib/htm/long_term_memory/tag_operations.rb,
lib/htm/long_term_memory/fulltext_search.rb,
lib/htm/long_term_memory/node_operations.rb,
lib/htm/long_term_memory/relevance_scorer.rb,
lib/htm/long_term_memory/robot_operations.rb

Overview

examples/robot_groups/lib/htm/working_memory_channel.rb frozen_string_literal: true

Defined Under Namespace

Modules: JobAdapter, Jobs, Loaders, MCP, Models, Observability, Sinatra, Telemetry, Workflows Classes: AuthorizationError, CircuitBreaker, CircuitBreakerOpenError, Config, ConfigurationError, Database, DatabaseError, EmbeddingError, EmbeddingService, Error, LongTermMemory, Migration, NotFoundError, PropositionError, PropositionService, QueryCache, QueryTimeoutError, Railtie, ResourceExhaustedError, RobotGroup, SequelConfig, SqlBuilder, TagError, TagService, Timeframe, TimeframeExtractor, ValidationError, WorkingMemory, WorkingMemoryChannel

Constant Summary collapse

MAX_KEY_LENGTH =

Validation constants

255
MAX_VALUE_LENGTH =

1MB

1_000_000
MAX_ARRAY_SIZE =
1000
VALID_RECALL_STRATEGIES =
%i[vector fulltext hybrid].freeze
VERSION =
'0.0.32'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(working_memory_size: 128_000, robot_name: nil, db_config: nil, db_pool_size: 5, db_query_timeout: 30_000, db_cache_size: 1000, db_cache_ttl: 300) ⇒ HTM

Initialize a new HTM instance

Parameters:

  • working_memory_size (Integer) (defaults to: 128_000)

    Maximum tokens for working memory (default: 128,000)

  • robot_name (String) (defaults to: nil)

    Human-readable name for this robot (auto-generated if not provided)

  • db_config (Hash) (defaults to: nil)

    Database configuration (uses ENV if not provided)

  • db_pool_size (Integer) (defaults to: 5)

    Database connection pool size (default: 5)

  • db_query_timeout (Integer) (defaults to: 30_000)

    Database query timeout in milliseconds (default: 30000)

  • db_cache_size (Integer) (defaults to: 1000)

    Number of database query results to cache (default: 1000, use 0 to disable)

  • db_cache_ttl (Integer) (defaults to: 300)

    Database cache TTL in seconds (default: 300)



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/htm.rb', line 81

def initialize(
  working_memory_size: 128_000,
  robot_name: nil,
  db_config: nil,
  db_pool_size: 5,
  db_query_timeout: 30_000,
  db_cache_size: 1000,
  db_cache_ttl: 300
)
  # Establish Sequel connection if not already connected
  HTM::SequelConfig.establish_connection! unless HTM::SequelConfig.db

  @robot_name = robot_name || "robot_#{SecureRandom.uuid[0..7]}"

  # Initialize components
  @working_memory = HTM::WorkingMemory.new(max_tokens: working_memory_size)
  @long_term_memory = HTM::LongTermMemory.new(
    db_config || HTM::Database.default_config,
    pool_size: db_pool_size,
    query_timeout: db_query_timeout,
    cache_size: db_cache_size,
    cache_ttl: db_cache_ttl
  )

  # Register this robot in the database and get its integer ID
  @robot_id = register_robot
end

Instance Attribute Details

#long_term_memoryObject (readonly)

Returns the value of attribute long_term_memory.



62
63
64
# File 'lib/htm.rb', line 62

def long_term_memory
  @long_term_memory
end

#robot_idObject (readonly)

Returns the value of attribute robot_id.



62
63
64
# File 'lib/htm.rb', line 62

def robot_id
  @robot_id
end

#robot_nameObject (readonly)

Returns the value of attribute robot_name.



62
63
64
# File 'lib/htm.rb', line 62

def robot_name
  @robot_name
end

#working_memoryObject (readonly)

Returns the value of attribute working_memory.



62
63
64
# File 'lib/htm.rb', line 62

def working_memory
  @working_memory
end

Class Method Details

.configHTM::Config Also known as: configuration

Get current configuration (singleton)

Returns:



681
682
683
# File 'lib/htm.rb', line 681

def config
  @config ||= Config.new
end

.configure {|config| ... } ⇒ Object

Configure HTM

Examples:

Custom configuration

HTM.configure do |config|
  config.embedding_generator = ->(text) { MyEmbedder.embed(text) }
  config.tag_extractor = ->(text, ontology) { MyTagger.extract(text, ontology) }
end

Default configuration

HTM.configure  # Uses RubyLLM defaults

Yields:

  • (config)

    Configuration object

Yield Parameters:



702
703
704
705
706
# File 'lib/htm.rb', line 702

def configure
  yield(config) if block_given?
  config.validate!
  config
end

.count_tokens(text) ⇒ Integer

Count tokens using configured counter

Parameters:

  • text (String)

    Text to count tokens for

Returns:

  • (Integer)

    Token count



779
780
781
782
783
# File 'lib/htm.rb', line 779

def count_tokens(text)
  config.token_counter.call(text)
rescue StandardError => e
  raise HTM::ValidationError, "Token counting failed: #{e.message}"
end

.dbSequel::Database

Convenience method to access the database connection

Returns:

  • (Sequel::Database)


213
214
215
# File 'lib/htm/sequel_config.rb', line 213

def self.db
  SequelConfig.db || SequelConfig.establish_connection!
end

.development?Boolean

Check if running in development environment

Returns:

  • (Boolean)


733
734
735
# File 'lib/htm.rb', line 733

def development?
  env == 'development'
end

.embed(text) ⇒ Array<Float>

Generate embedding using EmbeddingService

Parameters:

  • text (String)

    Text to embed

Returns:

  • (Array<Float>)

    Embedding vector (original, not padded)



750
751
752
753
# File 'lib/htm.rb', line 750

def embed(text)
  result = HTM::EmbeddingService.generate(text)
  result[:embedding]
end

.envString

Get current environment

Returns:

  • (String)

    Current environment name



717
718
719
# File 'lib/htm.rb', line 717

def env
  Config.env
end

.extract_propositions(text) ⇒ Array<String>

Extract propositions using PropositionService

Parameters:

  • text (String)

    Text to analyze

Returns:

  • (Array<String>)

    Extracted atomic propositions



770
771
772
# File 'lib/htm.rb', line 770

def extract_propositions(text)
  HTM::PropositionService.extract(text)
end

.extract_tags(text, existing_ontology: []) ⇒ Array<String>

Extract tags using TagService

Parameters:

  • text (String)

    Text to analyze

  • existing_ontology (Array<String>) (defaults to: [])

    Sample of existing tags for context

Returns:

  • (Array<String>)

    Extracted and validated tag names



761
762
763
# File 'lib/htm.rb', line 761

def extract_tags(text, existing_ontology: [])
  HTM::TagService.extract(text, existing_ontology: existing_ontology)
end

.loggerLogger

Get configured logger

Returns:

  • (Logger)

    Configured logger instance



789
790
791
# File 'lib/htm.rb', line 789

def logger
  config.logger
end

.production?Boolean

Check if running in production environment

Returns:

  • (Boolean)


741
742
743
# File 'lib/htm.rb', line 741

def production?
  env == 'production'
end

.reset_configuration!Object

Reset configuration to defaults



709
710
711
# File 'lib/htm.rb', line 709

def reset_configuration!
  @config = nil
end

.test?Boolean

Check if running in test environment

Returns:

  • (Boolean)


725
726
727
# File 'lib/htm.rb', line 725

def test?
  env == 'test'
end

Instance Method Details

#clear_working_memoryInteger

Clear all nodes from working memory

Marks all nodes as evicted from working memory (in database) and clears the in-memory cache. Nodes remain in long-term memory.

Examples:

htm.clear_working_memory  # => 5

Returns:

  • (Integer)

    Number of nodes cleared from working memory



422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/htm.rb', line 422

def clear_working_memory
  # Clear in-memory cache
  @working_memory.clear

  # Update database: mark all as evicted from working memory
  count = HTM::Models::RobotNode
          .where(robot_id: @robot_id, working_memory: true)
          .update(working_memory: false)

  HTM.logger.info "Cleared #{count} nodes from working memory"
  count
end

#forget(node_id, soft: true, confirm: false) ⇒ Boolean

Forget a memory node (soft delete by default, permanent delete requires confirmation)

By default, performs a soft delete (sets deleted_at timestamp). The node remains in the database but is excluded from queries. Use soft: false with confirm: :confirmed for permanent deletion.

Examples:

Soft delete (recoverable)

htm.forget(node_id)
htm.forget(node_id, soft: true)

Permanent delete (requires confirmation)

htm.forget(node_id, soft: false, confirm: :confirmed)

Parameters:

  • node_id (Integer)

    ID of the node to delete

  • soft (Boolean) (defaults to: true)

    If true (default), soft delete; if false, permanent delete

  • confirm (Symbol) (defaults to: false)

    Must be :confirmed to proceed with permanent deletion

Returns:

  • (Boolean)

    true if deleted

Raises:

  • (ArgumentError)

    if permanent deletion requested without confirmation

  • (HTM::NotFoundError)

    if node doesn’t exist



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/htm.rb', line 286

def forget(node_id, soft: true, confirm: false)
  # Validate inputs
  raise ArgumentError, "Node ID cannot be nil" if node_id.nil?

  # Permanent delete requires confirmation
  if !soft && confirm != :confirmed
    raise ArgumentError, "Permanent deletion requires confirm: :confirmed"
  end

  # Verify node exists (including soft-deleted for restore scenarios)
  node = HTM::Models::Node.with_deleted.first(id: node_id)
  raise HTM::NotFoundError, "Node not found: #{node_id}" unless node

  if soft
    # Soft delete - mark as deleted but keep in database
    node.soft_delete!
    @long_term_memory.clear_cache!  # Invalidate cache since node is no longer visible
    HTM.logger.info "Node #{node_id} soft deleted"
  else
    # Permanent delete (also invalidates cache internally)
    @long_term_memory.delete(node_id)
    HTM.logger.info "Node #{node_id} permanently deleted"
  end

  # Remove from working memory either way
  @working_memory.remove(node_id)

  update_robot_activity
  true
end

#forget_content(content_substring, soft: true, confirm: false) ⇒ Array<Integer>

Forget all nodes whose content includes the given string

Performs a soft delete on all matching nodes. The nodes remain in the database but are excluded from queries. Use case-insensitive LIKE matching.

Examples:

Soft delete all nodes containing “deprecated”

htm.forget_content("deprecated")
# => [42, 56, 78]  # IDs of deleted nodes

Permanent delete all nodes containing “test data”

htm.forget_content("test data", soft: false, confirm: :confirmed)

Parameters:

  • content_substring (String)

    Substring to search for in node content

  • soft (Boolean) (defaults to: true)

    If true (default), soft delete; if false, permanent delete

  • confirm (Symbol) (defaults to: false)

    Must be :confirmed to proceed with permanent deletion

Returns:

  • (Array<Integer>)

    Array of node IDs that were deleted

Raises:

  • (ArgumentError)

    if content_substring is blank

  • (ArgumentError)

    if permanent deletion requested without confirmation



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/htm.rb', line 336

def forget_content(content_substring, soft: true, confirm: false)
  raise ArgumentError, "Content substring cannot be blank" if content_substring.to_s.strip.empty?

  # Permanent delete requires confirmation
  if !soft && confirm != :confirmed
    raise ArgumentError, "Permanent deletion requires confirm: :confirmed"
  end

  # Find all nodes containing the substring (case-insensitive)
  matching_nodes = HTM::Models::Node.where(Sequel.ilike(:content, "%#{content_substring}%"))
  node_ids = matching_nodes.select_map(:id)

  if node_ids.empty?
    HTM.logger.info "No nodes found containing: #{content_substring}"
    return []
  end

  # Delete each matching node
  node_ids.each do |node_id|
    forget(node_id, soft: soft, confirm: confirm)
  end

  HTM.logger.info "Forgot #{node_ids.length} nodes containing: #{content_substring}"
  node_ids
end

#load_directory(path, pattern: '**/*.md', force: false) ⇒ Array<Hash>

Load all matching files from a directory into long-term memory

Examples:

Load all markdown files recursively

results = htm.load_directory('/path/to/docs')

Load only top-level markdown files

results = htm.load_directory('/path/to/docs', pattern: '*.md')

Parameters:

  • path (String)

    Directory path

  • pattern (String) (defaults to: '**/*.md')

    Glob pattern (default: ‘*/.md’)

  • force (Boolean) (defaults to: false)

    Force re-sync even if unchanged (default: false)

Returns:

  • (Array<Hash>)

    Results for each file



478
479
480
481
482
483
484
485
486
487
488
# File 'lib/htm.rb', line 478

def load_directory(path, pattern: '**/*.md', force: false)
  loader = HTM::Loaders::MarkdownLoader.new(self)
  results = loader.load_directory(path, pattern: pattern, force: force)

  # Update activity if any files were processed
  if results.any? { |r| !r[:skipped] && !r[:error] }
    update_robot_activity
  end

  results
end

#load_file(path, force: false) ⇒ Hash

Load a single file into long-term memory

Reads a text-based file (starting with markdown), chunks it by paragraph, and stores each chunk as a node. YAML frontmatter is preserved as metadata on the first chunk.

Examples:

Load a markdown file

result = htm.load_file('/path/to/doc.md')
# => { file_path: '/path/to/doc.md', chunks_created: 5, ... }

Force re-sync even if unchanged

result = htm.load_file('/path/to/doc.md', force: true)

Parameters:

  • path (String)

    Path to file

  • force (Boolean) (defaults to: false)

    Force re-sync even if mtime unchanged (default: false)

Returns:

  • (Hash)

    Result with keys:

    • :file_path [String] Absolute path to file

    • :chunks_created [Integer] Number of new chunks created

    • :chunks_updated [Integer] Number of existing chunks updated

    • :chunks_deleted [Integer] Number of chunks soft-deleted

    • :skipped [Boolean] True if file was unchanged and skipped



457
458
459
460
461
462
463
# File 'lib/htm.rb', line 457

def load_file(path, force: false)
  loader = HTM::Loaders::MarkdownLoader.new(self)
  result = loader.load_file(path, force: force)

  update_robot_activity unless result[:skipped]
  result
end

#nodes_from_file(file_path) ⇒ Array<HTM::Models::Node>

Get all nodes loaded from a specific file

Examples:

nodes = htm.nodes_from_file('/path/to/doc.md')
nodes.each { |node| puts node.content }

Parameters:

  • file_path (String)

    Path to the source file

Returns:



499
500
501
502
503
504
# File 'lib/htm.rb', line 499

def nodes_from_file(file_path)
  source = HTM::Models::FileSource.first(file_path: File.expand_path(file_path))
  return [] unless source

  HTM::Models::Node.from_source(source.id).all
end

#purge_deleted(older_than:, confirm: false) ⇒ Integer

Permanently delete all soft-deleted nodes older than specified time

Examples:

Purge nodes deleted more than 30 days ago

htm.purge_deleted(older_than: 30.days.ago, confirm: :confirmed)

Purge nodes deleted before a specific date

htm.purge_deleted(older_than: Time.new(2024, 1, 1), confirm: :confirmed)

Parameters:

  • older_than (Time, ActiveSupport::Duration)

    Purge nodes soft-deleted before this time

  • confirm (Symbol) (defaults to: false)

    Must be :confirmed to proceed

Returns:

  • (Integer)

    Number of nodes permanently deleted

Raises:

  • (ArgumentError)

    if confirmation not provided



403
404
405
406
407
408
409
410
# File 'lib/htm.rb', line 403

def purge_deleted(older_than:, confirm: false)
  raise ArgumentError, "Purge requires confirm: :confirmed" unless confirm == :confirmed

  count = HTM::Models::Node.purge_deleted(older_than: older_than)
  HTM.logger.info "Purged #{count} soft-deleted nodes older than #{older_than}"

  count
end

#recall(topic, timeframe: nil, limit: 20, strategy: :fulltext, with_relevance: false, query_tags: [], raw: false, metadata: {}) ⇒ Array<String>, Array<Hash>

Recall memories from a timeframe and topic

Examples:

Basic usage - no time filter (returns content strings)

memories = htm.recall("PostgreSQL")
# => ["PostgreSQL is great for time-series data", "PostgreSQL with TimescaleDB..."]

With explicit timeframe

memories = htm.recall("PostgreSQL", timeframe: "last week")
memories = htm.recall("PostgreSQL", timeframe: Date.today)
memories = htm.recall("PostgreSQL", timeframe: 7.days.ago..Time.now)

Auto-extract timeframe from query

memories = htm.recall("what did we discuss last week about PostgreSQL", timeframe: :auto)
# Extracts "last week" as timeframe, searches for "what did we discuss about PostgreSQL"

Multiple time windows

memories = htm.recall("meetings", timeframe: [last_monday, last_friday])

Filter by metadata

memories = htm.recall("preferences", metadata: { source: "user" })
memories = htm.recall("decisions", metadata: { confidence: 0.9, type: "architectural" })

Parameters:

  • topic (String)

    Topic to search for (required)

  • timeframe (nil, Range, Array<Range>, Date, DateTime, Time, String, Symbol) (defaults to: nil)

    Time filter

    • nil: No time filter (search all memories)

    • Range: Time range (e.g., 7.days.ago..Time.now)

    • Array<Range>: Multiple time windows (OR’d together)

    • Date: Entire day

    • DateTime/Time: Entire day containing that moment

    • String: Natural language (e.g., “last week”, “few days ago”)

    • :auto: Extract timeframe from topic query automatically

  • limit (Integer) (defaults to: 20)

    Maximum number of nodes to retrieve (default: 20)

  • strategy (Symbol) (defaults to: :fulltext)

    Search strategy (:vector, :fulltext, :hybrid) (default: :vector)

  • with_relevance (Boolean) (defaults to: false)

    Include dynamic relevance scores (default: false)

  • query_tags (Array<String>) (defaults to: [])

    Tags to boost relevance (default: [])

  • raw (Boolean) (defaults to: false)

    Return full node hashes (true) or just content strings (false) (default: false)

  • metadata (Hash) (defaults to: {})

    Filter by metadata fields using JSONB containment (default: {})

Returns:

  • (Array<String>, Array<Hash>)

    Content strings (raw: false) or full node hashes (raw: true)



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/htm.rb', line 196

def recall(topic, timeframe: nil, limit: 20, strategy: :fulltext, with_relevance: false, query_tags: [], raw: false, metadata: {})
  # Validate inputs
  validate_timeframe!(timeframe)
  validate_positive_integer!(limit, "limit")
  validate_recall_strategy!(strategy)
  validate_array!(query_tags, "query_tags")
  validate_metadata!()

  # Normalize timeframe and potentially extract from query
  search_query = topic
  normalized_timeframe = if timeframe == :auto
                           result = HTM::Timeframe.normalize(:auto, query: topic)
                           search_query = result.query  # Use cleaned query for search
                           result.timeframe
                         else
                           HTM::Timeframe.normalize(timeframe)
                         end

  # Use relevance-based search if requested
  nodes = if with_relevance
            @long_term_memory.search_with_relevance(
              timeframe: normalized_timeframe,
              query: search_query,
              query_tags: query_tags,
              limit: limit,
              embedding_service: %i[vector hybrid].include?(strategy) ? HTM : nil,
              metadata: 
            )
          else
            # Perform standard RAG-based retrieval
            case strategy
            when :vector
              # Vector search using query embedding
              @long_term_memory.search(
                timeframe: normalized_timeframe,
                query: search_query,
                limit: limit,
                embedding_service: HTM,
                metadata: 
              )
            when :fulltext
              @long_term_memory.search_fulltext(
                timeframe: normalized_timeframe,
                query: search_query,
                limit: limit,
                metadata: 
              )
            when :hybrid
              # Hybrid search combining vector + fulltext
              @long_term_memory.search_hybrid(
                timeframe: normalized_timeframe,
                query: search_query,
                limit: limit,
                embedding_service: HTM,
                metadata: 
              )
            end
          end

  # Add to working memory (evict if needed)
  nodes.each do |node|
    add_to_working_memory(node)
  end

  update_robot_activity

  # Return full nodes or just content based on raw parameter
  raw ? nodes : nodes.map { |node| node['content'] }
end

#remember(content, tags: [], metadata: {}) ⇒ Integer

Remember new information

Stores content in long-term memory and adds it to working memory. Embeddings and hierarchical tags are automatically extracted by LLM in the background.

Examples:

Remember with source

node_id = htm.remember("PostgreSQL is great for HTM")

Remember with manual tags

node_id = htm.remember("Time-series data", tags: ["database:timescaledb"])

Remember with metadata

node_id = htm.remember("User prefers dark mode", metadata: { source: "user", confidence: 0.95 })

Parameters:

  • content (String)

    The information to remember (required, cannot be nil or empty)

  • tags (Array<String>) (defaults to: [])

    Manual tags to assign (optional, in addition to auto-extracted tags)

Returns:

  • (Integer)

    Database ID of the memory node

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/htm.rb', line 128

def remember(content, tags: [], metadata: {})
  raise ValidationError, "Content cannot be nil" if content.nil?
  content = content.to_s.strip
  raise ValidationError, "Content cannot be empty" if content.empty?
  validate_remember_content!(content)
  validate_remember_tags!(tags)
  validate_metadata!()

  token_count = HTM.count_tokens(content)
  result = @long_term_memory.add(
    content: content, token_count: token_count, robot_id: @robot_id,
    embedding: nil, metadata: 
  )
  node_id = result[:node_id]

  if result[:is_new]
    HTM.logger.info "Node #{node_id} created for robot #{@robot_name} (#{token_count} tokens)"
    enqueue_background_jobs(node_id, tags: tags, metadata: )
  else
    rc = result[:robot_node].remember_count
    HTM.logger.info "Node #{node_id} already exists, linked to robot #{@robot_name} (remember_count: #{rc})"
    handle_existing_node_tags(node_id, tags)
  end

  store_in_working_memory(node_id, content, token_count: token_count, robot_node: result[:robot_node])
  update_robot_activity
  node_id
end

#restore(node_id) ⇒ Boolean

Restore a soft-deleted memory node

Examples:

htm.forget(node_id)        # Soft delete
htm.restore(node_id)       # Bring it back

Parameters:

  • node_id (Integer)

    ID of the soft-deleted node to restore

Returns:

  • (Boolean)

    true if restored

Raises:



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/htm.rb', line 372

def restore(node_id)
  raise ArgumentError, "Node ID cannot be nil" if node_id.nil?

  # Find including soft-deleted nodes
  node = HTM::Models::Node.with_deleted.first(id: node_id)
  raise HTM::NotFoundError, "Node not found: #{node_id}" unless node

  unless node.deleted?
    raise ArgumentError, "Node #{node_id} is not soft-deleted"
  end

  node.restore!
  HTM.logger.info "Node #{node_id} restored"

  update_robot_activity
  true
end

#unload_file(file_path) ⇒ Integer

Unload a file (soft-delete all its chunks and remove source record)

Examples:

count = htm.unload_file('/path/to/doc.md')
puts "Unloaded #{count} chunks"

Parameters:

  • file_path (String)

    Path to the source file

Returns:

  • (Integer)

    Number of nodes soft-deleted



515
516
517
518
519
520
521
522
523
524
525
# File 'lib/htm.rb', line 515

def unload_file(file_path)
  source = HTM::Models::FileSource.first(file_path: File.expand_path(file_path))
  return 0 unless source

  count = source.soft_delete_chunks!
  @long_term_memory.clear_cache!
  source.delete

  update_robot_activity
  count
end