htm 0.0.18 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -1
  3. data/README.md +12 -0
  4. data/Rakefile +104 -18
  5. data/db/migrate/00001_enable_extensions.rb +9 -5
  6. data/db/migrate/00002_create_robots.rb +18 -6
  7. data/db/migrate/00003_create_file_sources.rb +30 -17
  8. data/db/migrate/00004_create_nodes.rb +60 -48
  9. data/db/migrate/00005_create_tags.rb +24 -12
  10. data/db/migrate/00006_create_node_tags.rb +28 -13
  11. data/db/migrate/00007_create_robot_nodes.rb +40 -26
  12. data/db/schema.sql +17 -1
  13. data/db/seeds.rb +34 -34
  14. data/docs/api/embedding-service.md +140 -110
  15. data/docs/api/yard/HTM/ActiveRecordConfig.md +6 -0
  16. data/docs/api/yard/HTM/Config.md +173 -0
  17. data/docs/api/yard/HTM/ConfigSection.md +28 -0
  18. data/docs/api/yard/HTM/Database.md +1 -1
  19. data/docs/api/yard/HTM/Railtie.md +2 -2
  20. data/docs/api/yard/HTM.md +0 -57
  21. data/docs/api/yard/index.csv +76 -61
  22. data/docs/api/yard-reference.md +2 -1
  23. data/docs/architecture/adrs/003-ollama-embeddings.md +45 -36
  24. data/docs/architecture/adrs/004-hive-mind.md +1 -1
  25. data/docs/architecture/adrs/008-robot-identification.md +1 -1
  26. data/docs/architecture/index.md +11 -9
  27. data/docs/architecture/overview.md +11 -7
  28. data/docs/assets/images/balanced-strategy-decay.svg +41 -0
  29. data/docs/assets/images/class-hierarchy.svg +1 -1
  30. data/docs/assets/images/eviction-priority.svg +43 -0
  31. data/docs/assets/images/exception-hierarchy.svg +2 -2
  32. data/docs/assets/images/hive-mind-shared-memory.svg +52 -0
  33. data/docs/assets/images/htm-architecture-overview.svg +3 -3
  34. data/docs/assets/images/htm-core-components.svg +4 -4
  35. data/docs/assets/images/htm-layered-architecture.svg +1 -1
  36. data/docs/assets/images/htm-memory-addition-flow.svg +2 -2
  37. data/docs/assets/images/htm-memory-recall-flow.svg +2 -2
  38. data/docs/assets/images/memory-topology.svg +53 -0
  39. data/docs/assets/images/two-tier-memory-architecture.svg +55 -0
  40. data/docs/database/naming-convention.md +244 -0
  41. data/docs/database_rake_tasks.md +31 -0
  42. data/docs/development/rake-tasks.md +80 -35
  43. data/docs/development/setup.md +76 -44
  44. data/docs/examples/basic-usage.md +133 -0
  45. data/docs/examples/config-files.md +170 -0
  46. data/docs/examples/file-loading.md +208 -0
  47. data/docs/examples/index.md +116 -0
  48. data/docs/examples/llm-configuration.md +168 -0
  49. data/docs/examples/mcp-client.md +172 -0
  50. data/docs/examples/rails-integration.md +173 -0
  51. data/docs/examples/robot-groups.md +210 -0
  52. data/docs/examples/sinatra-integration.md +218 -0
  53. data/docs/examples/standalone-app.md +216 -0
  54. data/docs/examples/telemetry.md +224 -0
  55. data/docs/examples/timeframes.md +143 -0
  56. data/docs/getting-started/installation.md +97 -40
  57. data/docs/getting-started/quick-start.md +28 -11
  58. data/docs/guides/configuration.md +515 -0
  59. data/docs/guides/file-loading.md +322 -0
  60. data/docs/guides/getting-started.md +40 -9
  61. data/docs/guides/index.md +3 -3
  62. data/docs/guides/mcp-server.md +100 -13
  63. data/docs/guides/propositions.md +264 -0
  64. data/docs/guides/recalling-memories.md +4 -4
  65. data/docs/guides/search-strategies.md +3 -3
  66. data/docs/guides/tags.md +318 -0
  67. data/docs/guides/telemetry.md +229 -0
  68. data/docs/index.md +8 -16
  69. data/docs/{architecture → robots}/hive-mind.md +8 -111
  70. data/docs/robots/index.md +73 -0
  71. data/docs/{guides → robots}/multi-robot.md +3 -3
  72. data/docs/{guides → robots}/robot-groups.md +8 -7
  73. data/docs/{architecture → robots}/two-tier-memory.md +13 -149
  74. data/docs/robots/why-robots.md +85 -0
  75. data/examples/.envrc +6 -0
  76. data/examples/.gitignore +2 -0
  77. data/examples/00_create_examples_db.rb +94 -0
  78. data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
  79. data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
  80. data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
  81. data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
  82. data/examples/{example_app → 06_example_app}/app.rb +15 -15
  83. data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
  84. data/examples/08_sinatra_app/Gemfile.lock +241 -0
  85. data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
  86. data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
  87. data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
  88. data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
  89. data/examples/11_robot_groups/README.md +335 -0
  90. data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
  91. data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
  92. data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
  93. data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
  94. data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
  95. data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
  96. data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
  97. data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
  98. data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
  99. data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
  100. data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
  101. data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
  102. data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
  103. data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
  104. data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
  105. data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
  106. data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
  107. data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
  108. data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
  109. data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
  110. data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
  111. data/examples/12_rails_app/config/initializers/htm.rb +7 -0
  112. data/examples/12_rails_app/config/initializers/rack.rb +5 -0
  113. data/examples/README.md +230 -211
  114. data/examples/examples_helper.rb +138 -0
  115. data/lib/htm/config/builder.rb +167 -0
  116. data/lib/htm/config/database.rb +317 -0
  117. data/lib/htm/config/defaults.yml +41 -13
  118. data/lib/htm/config/section.rb +74 -0
  119. data/lib/htm/config/validator.rb +83 -0
  120. data/lib/htm/config.rb +65 -361
  121. data/lib/htm/database.rb +85 -127
  122. data/lib/htm/errors.rb +14 -0
  123. data/lib/htm/integrations/sinatra.rb +13 -44
  124. data/lib/htm/job_adapter.rb +75 -1
  125. data/lib/htm/jobs/generate_embedding_job.rb +3 -4
  126. data/lib/htm/jobs/generate_propositions_job.rb +4 -5
  127. data/lib/htm/jobs/generate_tags_job.rb +16 -15
  128. data/lib/htm/loaders/defaults_loader.rb +23 -0
  129. data/lib/htm/loaders/markdown_loader.rb +17 -15
  130. data/lib/htm/loaders/xdg_config_loader.rb +9 -9
  131. data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
  132. data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
  133. data/lib/htm/long_term_memory/node_operations.rb +24 -23
  134. data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
  135. data/lib/htm/long_term_memory/robot_operations.rb +4 -4
  136. data/lib/htm/long_term_memory/tag_operations.rb +91 -77
  137. data/lib/htm/long_term_memory/vector_search.rb +4 -5
  138. data/lib/htm/long_term_memory.rb +13 -13
  139. data/lib/htm/mcp/cli.rb +115 -8
  140. data/lib/htm/mcp/resources.rb +4 -3
  141. data/lib/htm/mcp/server.rb +5 -4
  142. data/lib/htm/mcp/tools.rb +37 -28
  143. data/lib/htm/migration.rb +72 -0
  144. data/lib/htm/models/file_source.rb +52 -31
  145. data/lib/htm/models/node.rb +224 -108
  146. data/lib/htm/models/node_tag.rb +49 -28
  147. data/lib/htm/models/robot.rb +38 -27
  148. data/lib/htm/models/robot_node.rb +63 -35
  149. data/lib/htm/models/tag.rb +126 -123
  150. data/lib/htm/observability.rb +45 -41
  151. data/lib/htm/proposition_service.rb +76 -7
  152. data/lib/htm/railtie.rb +2 -2
  153. data/lib/htm/robot_group.rb +30 -18
  154. data/lib/htm/sequel_config.rb +215 -0
  155. data/lib/htm/sql_builder.rb +14 -16
  156. data/lib/htm/tag_service.rb +78 -0
  157. data/lib/htm/tasks.rb +3 -0
  158. data/lib/htm/version.rb +1 -1
  159. data/lib/htm/workflows/remember_workflow.rb +213 -0
  160. data/lib/htm.rb +27 -22
  161. data/lib/tasks/db.rake +0 -2
  162. data/lib/tasks/doc.rake +2 -2
  163. data/lib/tasks/files.rake +11 -18
  164. data/lib/tasks/htm.rake +190 -62
  165. data/lib/tasks/jobs.rake +179 -54
  166. data/lib/tasks/tags.rake +8 -13
  167. data/mkdocs.yml +33 -8
  168. data/scripts/backfill_parent_tags.rb +376 -0
  169. data/scripts/normalize_plural_tags.rb +335 -0
  170. metadata +168 -86
  171. data/docs/api/yard/HTM/Configuration.md +0 -240
  172. data/docs/telemetry.md +0 -391
  173. data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
  174. data/examples/sinatra_app/Gemfile.lock +0 -166
  175. data/lib/htm/active_record_config.rb +0 -104
  176. /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
  177. /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
  178. /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
  179. /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
  180. /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
  181. /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
  182. /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
  183. /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
  184. /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
  185. /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
  186. /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
  187. /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
  188. /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
  189. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
  190. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
  191. /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
  192. /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
  193. /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
  194. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
  195. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
  196. /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
  197. /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
  198. /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
  199. /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
  200. /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
  201. /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
  202. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
  203. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
  204. /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
  205. /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
  206. /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
  207. /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
  208. /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
  209. /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
  210. /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
  211. /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
  212. /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
  213. /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
  214. /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
  215. /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
  216. /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
data/lib/htm/mcp/cli.rb CHANGED
@@ -21,6 +21,7 @@ class HTM
21
21
  verify Verify database connection and extensions
22
22
  stats Show memory statistics
23
23
  config Output default configuration to STDOUT
24
+ rake Run HTM rake tasks (use -T [pattern] to list)
24
25
  version Show HTM version
25
26
  help Show this help message
26
27
 
@@ -107,6 +108,19 @@ class HTM
107
108
  # Start MCP server (for Claude Desktop)
108
109
  htm_mcp
109
110
 
111
+ # List available rake tasks
112
+ htm_mcp rake -T
113
+ htm_mcp rake --tasks
114
+
115
+ # List tasks matching a pattern
116
+ htm_mcp rake -T htm:jobs
117
+ htm_mcp rake -T db
118
+
119
+ # Run rake tasks
120
+ htm_mcp rake htm:db:stats
121
+ htm_mcp rake htm:tags:tree
122
+ htm_mcp rake 'htm:tags:tree[database]'
123
+
110
124
  CLAUDE DESKTOP CONFIGURATION:
111
125
  Add to ~/.config/claude/claude_desktop_config.json:
112
126
 
@@ -243,13 +257,13 @@ class HTM
243
257
  }
244
258
  end.sort_by { |m| m[:version] }
245
259
 
246
- # Ensure ActiveRecord connection for migration check
247
- HTM::ActiveRecordConfig.establish_connection!
260
+ # Ensure Sequel connection for migration check
261
+ HTM::SequelConfig.establish_connection!
248
262
 
249
263
  # Get applied migrations from database
250
264
  applied_versions = begin
251
- ActiveRecord::Base.connection.select_values('SELECT version FROM schema_migrations ORDER BY version')
252
- rescue ActiveRecord::StatementInvalid
265
+ HTM.db[:schema_migrations].select_order_map(:version)
266
+ rescue Sequel::DatabaseError
253
267
  []
254
268
  end
255
269
 
@@ -379,7 +393,7 @@ class HTM
379
393
  check_database_config!
380
394
 
381
395
  begin
382
- HTM::ActiveRecordConfig.establish_connection!
396
+ HTM::SequelConfig.establish_connection!
383
397
 
384
398
  total_nodes = HTM::Models::Node.count
385
399
  deleted_nodes = HTM::Models::Node.deleted.count
@@ -389,9 +403,9 @@ class HTM
389
403
  total_files = HTM::Models::FileSource.count
390
404
 
391
405
  # Get database size
392
- db_size = ActiveRecord::Base.connection.execute(
406
+ db_size = HTM.db.fetch(
393
407
  "SELECT pg_size_pretty(pg_database_size(current_database())) AS size"
394
- ).first['size']
408
+ ).first[:size]
395
409
 
396
410
  puts "Nodes: #{total_nodes} active, #{deleted_nodes} deleted, #{with_embeddings} with embeddings"
397
411
  puts "Tags: #{total_tags}"
@@ -427,6 +441,8 @@ class HTM
427
441
  run_stats
428
442
  when 'config'
429
443
  output_default_config
444
+ when 'rake'
445
+ run_rake(args[1..] || [])
430
446
  when 'server', 'stdio', nil
431
447
  # Return false to indicate server should start
432
448
  # 'stdio' is accepted for compatibility with MCP clients that pass it as an argument
@@ -468,7 +484,98 @@ class HTM
468
484
  end
469
485
 
470
486
  def command?(arg)
471
- %w[help version setup init verify stats config server stdio].include?(arg.downcase)
487
+ %w[help version setup init verify stats config server stdio rake].include?(arg.downcase)
488
+ end
489
+
490
+ def run_rake(args)
491
+ require 'rake'
492
+
493
+ # Handle --tasks / -T to list available tasks (with optional pattern)
494
+ if args.empty? || args.first == '--tasks' || args.first == '-T'
495
+ # Check for optional pattern after -T/--tasks
496
+ pattern = nil
497
+ if args.first == '--tasks' || args.first == '-T'
498
+ pattern = args[1] # May be nil if no pattern provided
499
+ end
500
+ list_rake_tasks(pattern: pattern)
501
+ return
502
+ end
503
+
504
+ task_name = args.shift
505
+
506
+ # Load HTM rake tasks
507
+ load_htm_rake_tasks
508
+
509
+ # Check if task exists
510
+ unless Rake::Task.task_defined?(task_name)
511
+ warn "Unknown rake task: #{task_name}"
512
+ warn "Run 'htm_mcp rake --tasks' to see available tasks."
513
+ exit 1
514
+ end
515
+
516
+ # Set remaining args as task arguments if any
517
+ # Rake tasks use ARGV for arguments in brackets like task[arg1,arg2]
518
+ begin
519
+ Rake::Task[task_name].invoke
520
+ rescue => e
521
+ warn "Rake task failed: #{e.message}"
522
+ warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
523
+ exit 1
524
+ end
525
+ end
526
+
527
+ def load_htm_rake_tasks
528
+ # Clear any existing tasks to avoid conflicts
529
+ Rake::TaskManager.record_task_metadata = true
530
+ Rake.application = Rake::Application.new
531
+ Rake.application.init('htm_mcp')
532
+
533
+ # Load all HTM task files
534
+ tasks_dir = File.expand_path('../../tasks', __dir__)
535
+ Dir.glob(File.join(tasks_dir, '*.rake')).sort.each do |rake_file|
536
+ load rake_file
537
+ end
538
+ end
539
+
540
+ def list_rake_tasks(pattern: nil)
541
+ load_htm_rake_tasks
542
+
543
+ # Collect tasks with descriptions, sorted by name
544
+ tasks = Rake.application.tasks
545
+ .select { |t| t.comment && t.name.start_with?('htm:') }
546
+ .sort_by(&:name)
547
+
548
+ # Filter by pattern if provided (matches task name)
549
+ if pattern
550
+ tasks = tasks.select { |t| t.name.include?(pattern) }
551
+ end
552
+
553
+ if tasks.empty?
554
+ if pattern
555
+ puts "No HTM rake tasks matching '#{pattern}'"
556
+ else
557
+ puts "No HTM rake tasks found"
558
+ end
559
+ return
560
+ end
561
+
562
+ if pattern
563
+ puts "HTM rake tasks matching '#{pattern}':"
564
+ else
565
+ puts "Available HTM rake tasks:"
566
+ end
567
+ puts
568
+
569
+ # Find max task name length for alignment
570
+ max_len = tasks.map { |t| t.name.length }.max || 0
571
+
572
+ tasks.each do |task|
573
+ printf " %-#{max_len}s # %s\n", task.name, task.comment
574
+ end
575
+
576
+ puts
577
+ puts "Run with: htm_mcp rake <task_name>"
578
+ puts "Example: htm_mcp rake htm:db:stats"
472
579
  end
473
580
  end
474
581
  end
@@ -51,7 +51,7 @@ class HTM
51
51
  mime_type "text/plain"
52
52
 
53
53
  def content
54
- HTM::Models::Tag.all.tree_string
54
+ HTM::Models::Tag.tree_string
55
55
  end
56
56
  end
57
57
 
@@ -62,9 +62,10 @@ class HTM
62
62
  mime_type "application/json"
63
63
 
64
64
  def content
65
- recent = HTM::Models::Node.includes(:tags)
66
- .order(created_at: :desc)
65
+ recent = HTM::Models::Node.eager(:tags)
66
+ .order(Sequel.desc(:created_at))
67
67
  .limit(20)
68
+ .all
68
69
  .map do |node|
69
70
  {
70
71
  id: node.id,
@@ -37,9 +37,9 @@ class HTM
37
37
  end
38
38
 
39
39
  def verify_database_connection!
40
- HTM::ActiveRecordConfig.establish_connection!
40
+ HTM::SequelConfig.establish_connection!
41
41
  # Quick connectivity test
42
- ActiveRecord::Base.connection.execute("SELECT 1")
42
+ HTM.db.execute("SELECT 1")
43
43
  rescue => e
44
44
  warn "Error: Cannot connect to database."
45
45
  warn e.message
@@ -70,8 +70,9 @@ class HTM
70
70
 
71
71
  def configure_htm!
72
72
  HTM.configure do |config|
73
- config.job.backend = :inline # Synchronous for MCP responses
74
- config.logger = @silent_logger # Silent logging for MCP
73
+ # Job backend now comes from config (defaults to :fiber)
74
+ # Use HTM_JOB__BACKEND=inline or config file to override
75
+ config.logger = @silent_logger # Silent logging for MCP
75
76
  end
76
77
  end
77
78
 
data/lib/htm/mcp/tools.rb CHANGED
@@ -45,7 +45,7 @@ class HTM
45
45
  Session.logger&.info "SetRobotTool called: name=#{name.inspect}"
46
46
 
47
47
  htm = Session.set_robot(name)
48
- robot = HTM::Models::Robot.find(htm.robot_id)
48
+ robot = HTM::Models::Robot[htm.robot_id]
49
49
 
50
50
  {
51
51
  success: true,
@@ -68,7 +68,7 @@ class HTM
68
68
  Session.logger&.info "GetRobotTool called"
69
69
 
70
70
  htm = Session.htm_instance
71
- robot = HTM::Models::Robot.find(htm.robot_id)
71
+ robot = HTM::Models::Robot[htm.robot_id]
72
72
 
73
73
  {
74
74
  success: true,
@@ -89,26 +89,27 @@ class HTM
89
89
 
90
90
  def call
91
91
  htm = Session.htm_instance
92
- robot = HTM::Models::Robot.find(htm.robot_id)
92
+ robot = HTM::Models::Robot[htm.robot_id]
93
93
  Session.logger&.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
94
94
 
95
95
  # Get all nodes in working memory with their metadata
96
- # Filter out any robot_nodes where the node has been deleted (node uses default_scope)
97
- working_memory_nodes = robot.robot_nodes
96
+ # Filter out any robot_nodes where the node has been deleted
97
+ working_memory_nodes = robot.robot_nodes_dataset
98
98
  .in_working_memory
99
- .joins(:node) # Inner join excludes deleted nodes
100
- .includes(node: :tags)
101
- .order(last_remembered_at: :desc)
99
+ .eager(node: :tags)
100
+ .order(Sequel.desc(:last_remembered_at))
101
+ .all
102
102
  .filter_map do |rn|
103
- next unless rn.node # Extra safety check
103
+ node = rn.node
104
+ next unless node # Exclude if node is nil (was deleted)
104
105
 
105
106
  {
106
- id: rn.node.id,
107
- content: rn.node.content,
108
- tags: rn.node.tags.map(&:name),
107
+ id: node.id,
108
+ content: node.content,
109
+ tags: node.tags.map(&:name),
109
110
  remember_count: rn.remember_count,
110
111
  last_remembered_at: rn.last_remembered_at&.iso8601,
111
- created_at: rn.node.created_at.iso8601
112
+ created_at: node.created_at.iso8601
112
113
  }
113
114
  end
114
115
 
@@ -142,7 +143,7 @@ class HTM
142
143
 
143
144
  htm = Session.htm_instance
144
145
  node_id = htm.remember(content, tags: tags, metadata: metadata)
145
- node = HTM::Models::Node.includes(:tags).find(node_id)
146
+ node = HTM::Models::Node.eager(:tags).first!(id: node_id)
146
147
 
147
148
  Session.logger&.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
148
149
 
@@ -187,7 +188,8 @@ class HTM
187
188
  memories = htm.recall(query, **recall_opts)
188
189
 
189
190
  results = memories.map do |memory|
190
- node = HTM::Models::Node.includes(:tags).find(memory['id'])
191
+ node = HTM::Models::Node.eager(:tags).first(id: memory['id'])
192
+ next unless node
191
193
  {
192
194
  id: memory['id'],
193
195
  content: memory['content'],
@@ -195,7 +197,7 @@ class HTM
195
197
  created_at: memory['created_at'],
196
198
  score: memory['combined_score'] || memory['similarity']
197
199
  }
198
- end
200
+ end.compact
199
201
 
200
202
  Session.logger&.info "Recall complete: found #{results.length} memories"
201
203
 
@@ -212,13 +214,17 @@ class HTM
212
214
  private
213
215
 
214
216
  def parse_timeframe(timeframe)
217
+ now = Time.now
215
218
  case timeframe.downcase
216
219
  when 'today'
217
- Time.now.beginning_of_day..Time.now
220
+ # Beginning of today
221
+ Time.new(now.year, now.month, now.day)..now
218
222
  when 'this week'
219
- 1.week.ago..Time.now
223
+ # 7 days ago
224
+ (now - 7 * 24 * 60 * 60)..now
220
225
  when 'this month'
221
- 1.month.ago..Time.now
226
+ # 30 days ago
227
+ (now - 30 * 24 * 60 * 60)..now
222
228
  else
223
229
  # Try to parse as ISO8601 range (start..end)
224
230
  if timeframe.include?('..')
@@ -226,7 +232,7 @@ class HTM
226
232
  Time.parse(parts[0])..Time.parse(parts[1])
227
233
  else
228
234
  # Single date - from that date to now
229
- Time.parse(timeframe)..Time.now
235
+ Time.parse(timeframe)..now
230
236
  end
231
237
  end
232
238
  rescue ArgumentError
@@ -256,7 +262,7 @@ class HTM
256
262
  robot_name: htm.robot_name,
257
263
  message: "Memory soft-deleted. Use restore to recover."
258
264
  }.to_json
259
- rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
265
+ rescue HTM::NotFoundError, Sequel::NoMatchingRow
260
266
  Session.logger&.warn "ForgetTool failed: node #{node_id} not found"
261
267
  {
262
268
  success: false,
@@ -287,7 +293,7 @@ class HTM
287
293
  robot_name: htm.robot_name,
288
294
  message: "Memory restored successfully"
289
295
  }.to_json
290
- rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
296
+ rescue HTM::NotFoundError, Sequel::NoMatchingRow
291
297
  Session.logger&.warn "RestoreTool failed: node #{node_id} not found"
292
298
  {
293
299
  success: false,
@@ -308,7 +314,7 @@ class HTM
308
314
  Session.logger&.info "ListTagsTool called: prefix=#{prefix.inspect}"
309
315
 
310
316
  tags_query = HTM::Models::Tag.order(:name)
311
- tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
317
+ tags_query = tags_query.where(Sequel.like(:name, "#{prefix}%")) if prefix
312
318
 
313
319
  tags = tags_query.map do |tag|
314
320
  {
@@ -348,7 +354,7 @@ class HTM
348
354
 
349
355
  # Enrich with node counts
350
356
  tags = results.map do |result|
351
- tag = HTM::Models::Tag.find_by(name: result[:name])
357
+ tag = HTM::Models::Tag.first(name: result[:name])
352
358
  {
353
359
  name: result[:name],
354
360
  similarity: result[:similarity].round(3),
@@ -396,7 +402,7 @@ class HTM
396
402
 
397
403
  # Enrich with tags
398
404
  results = nodes.map do |node_attrs|
399
- node = HTM::Models::Node.includes(:tags).find_by(id: node_attrs['id'])
405
+ node = HTM::Models::Node.eager(:tags).first(id: node_attrs['id'])
400
406
  next unless node
401
407
 
402
408
  {
@@ -429,14 +435,17 @@ class HTM
429
435
 
430
436
  def call
431
437
  htm = Session.htm_instance
432
- robot = HTM::Models::Robot.find(htm.robot_id)
438
+ robot = HTM::Models::Robot[htm.robot_id]
433
439
  Session.logger&.info "StatsTool called for robot=#{htm.robot_name}"
434
440
 
435
- # Note: Node uses default_scope to exclude deleted, so .count returns active nodes
441
+ # Note: Node uses set_dataset to exclude deleted, so .count returns active nodes
436
442
  total_nodes = HTM::Models::Node.count
437
443
  deleted_nodes = HTM::Models::Node.deleted.count
438
444
  nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
439
- nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
445
+ nodes_with_tags = HTM::Models::Node
446
+ .join(:node_tags, node_id: :id)
447
+ .distinct
448
+ .count
440
449
  total_tags = HTM::Models::Tag.count
441
450
  total_robots = HTM::Models::Robot.count
442
451
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HTM
4
+ # Base class for Sequel migrations
5
+ #
6
+ # Provides a simple interface for writing migrations compatible with
7
+ # HTM's migration runner.
8
+ #
9
+ # @example
10
+ # class CreateUsers < HTM::Migration
11
+ # def up
12
+ # create_table(:users) do
13
+ # primary_key :id
14
+ # String :name, null: false
15
+ # DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
16
+ # end
17
+ # end
18
+ #
19
+ # def down
20
+ # drop_table(:users)
21
+ # end
22
+ # end
23
+ #
24
+ class Migration
25
+ attr_reader :db
26
+
27
+ def initialize(db)
28
+ @db = db
29
+ end
30
+
31
+ # Override in subclass
32
+ def up
33
+ raise NotImplementedError, "#{self.class}#up must be implemented"
34
+ end
35
+
36
+ # Override in subclass (optional for irreversible migrations)
37
+ def down
38
+ raise NotImplementedError, "#{self.class}#down must be implemented"
39
+ end
40
+
41
+ private
42
+
43
+ # Delegate common methods to db
44
+ def create_table(name, **options, &block)
45
+ db.create_table(name, **options, &block)
46
+ end
47
+
48
+ def drop_table(name, **options)
49
+ db.drop_table(name, **options)
50
+ end
51
+
52
+ def alter_table(name, &block)
53
+ db.alter_table(name, &block)
54
+ end
55
+
56
+ def add_index(table, columns, **options)
57
+ db.add_index(table, columns, **options)
58
+ end
59
+
60
+ def drop_index(table, columns, **options)
61
+ db.drop_index(table, columns, **options)
62
+ end
63
+
64
+ def run(sql)
65
+ db.run(sql)
66
+ end
67
+
68
+ def execute(sql)
69
+ db.run(sql)
70
+ end
71
+ end
72
+ end
@@ -7,42 +7,44 @@ class HTM
7
7
  # Represents a file that has been loaded into HTM with its metadata.
8
8
  # Each file can have multiple associated nodes (chunks).
9
9
  #
10
- # @example Find source by path
11
- # source = FileSource.by_path('/path/to/doc.md').first
12
- # source.chunks # => [Node, Node, ...]
13
- #
14
- # @example Check if re-sync needed
15
- # current_mtime = File.mtime('/path/to/doc.md')
16
- # source.needs_sync?(current_mtime) # => true/false
17
- #
18
- class FileSource < ActiveRecord::Base
19
- self.table_name = 'file_sources'
20
-
10
+ class FileSource < Sequel::Model(:file_sources)
21
11
  # Tolerance for mtime comparison to avoid false positives from
22
12
  # precision differences between filesystem and database timestamps
23
13
  DELTA_TIME = 5 # seconds
24
14
 
25
15
  # Associations
26
- has_many :nodes, class_name: 'HTM::Models::Node',
27
- foreign_key: :source_id, dependent: :nullify
16
+ one_to_many :nodes, class: 'HTM::Models::Node', key: :source_id
17
+
18
+ # Plugins
19
+ plugin :validation_helpers
20
+ plugin :timestamps, update_on_create: true
28
21
 
29
22
  # Validations
30
- validates :file_path, presence: true, uniqueness: true
23
+ def validate
24
+ super
25
+ validates_presence :file_path
26
+ validates_unique :file_path
27
+ end
31
28
 
32
- # Scopes
33
- scope :by_path, ->(path) { where(file_path: File.expand_path(path)) }
34
- scope :stale, -> { where('mtime < last_synced_at') }
35
- scope :recently_synced, -> { order(last_synced_at: :desc) }
29
+ # Dataset methods (scopes)
30
+ dataset_module do
31
+ def by_path(path)
32
+ where(file_path: File.expand_path(path))
33
+ end
34
+
35
+ def stale
36
+ where(Sequel.lit('mtime < last_synced_at'))
37
+ end
38
+
39
+ def recently_synced
40
+ order(Sequel.desc(:last_synced_at))
41
+ end
42
+ end
36
43
 
37
44
  # Check if file needs re-sync based on mtime
38
45
  #
39
- # Uses DELTA_TIME tolerance to avoid false positives from:
40
- # - Nanosecond/microsecond precision differences (filesystem vs PostgreSQL)
41
- # - Floating-point rounding errors
42
- # - Minor timestamp discrepancies across systems
43
- #
44
- # @param current_mtime [Time, nil] Current file modification time (defaults to reading from filesystem)
45
- # @return [Boolean] true if file modification time differs by more than DELTA_TIME, or file doesn't exist
46
+ # @param current_mtime [Time, nil] Current file modification time
47
+ # @return [Boolean] true if file needs re-sync
46
48
  #
47
49
  def needs_sync?(current_mtime = nil)
48
50
  return true if mtime.nil?
@@ -54,10 +56,18 @@ class HTM
54
56
 
55
57
  # Get ordered chunks from this file
56
58
  #
57
- # @return [ActiveRecord::Relation] Nodes ordered by chunk_position
59
+ # @return [Array<Node>] Nodes ordered by chunk_position
58
60
  #
59
61
  def chunks
60
- nodes.order(:chunk_position)
62
+ nodes_dataset.order(:chunk_position).all
63
+ end
64
+
65
+ # Alias for nodes_dataset - used for consistency with "chunks" terminology
66
+ #
67
+ # @return [Sequel::Dataset] Dataset of nodes from this file
68
+ #
69
+ def chunks_dataset
70
+ nodes_dataset
61
71
  end
62
72
 
63
73
  # Extract tags from frontmatter
@@ -65,7 +75,7 @@ class HTM
65
75
  # @return [Array<String>] Tag names from frontmatter 'tags' field
66
76
  #
67
77
  def frontmatter_tags
68
- return [] unless frontmatter.is_a?(Hash)
78
+ return [] unless frontmatter_hash?
69
79
 
70
80
  tags = frontmatter['tags'] || frontmatter[:tags] || []
71
81
  Array(tags).map(&:to_s)
@@ -76,7 +86,7 @@ class HTM
76
86
  # @return [String, nil] Title from frontmatter
77
87
  #
78
88
  def title
79
- return nil unless frontmatter.is_a?(Hash)
89
+ return nil unless frontmatter_hash?
80
90
  frontmatter['title'] || frontmatter[:title]
81
91
  end
82
92
 
@@ -85,7 +95,7 @@ class HTM
85
95
  # @return [String, nil] Author from frontmatter
86
96
  #
87
97
  def author
88
- return nil unless frontmatter.is_a?(Hash)
98
+ return nil unless frontmatter_hash?
89
99
  frontmatter['author'] || frontmatter[:author]
90
100
  end
91
101
 
@@ -94,7 +104,18 @@ class HTM
94
104
  # @return [Integer] Number of chunks soft-deleted
95
105
  #
96
106
  def soft_delete_chunks!
97
- nodes.update_all(deleted_at: Time.current)
107
+ nodes_dataset.update(deleted_at: Time.now)
108
+ end
109
+
110
+ private
111
+
112
+ # Check if frontmatter is a hash-like object
113
+ # Sequel::Postgres::JSONBHash doesn't inherit from Hash but acts like one
114
+ #
115
+ # @return [Boolean]
116
+ #
117
+ def frontmatter_hash?
118
+ frontmatter.respond_to?(:[]) && frontmatter.respond_to?(:key?)
98
119
  end
99
120
  end
100
121
  end