lex-apollo 0.4.16 → 0.4.18

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c57e41cb0b9936b5b3933e847385ff6eed5581c29b2bd53ee15bab8d04972634
4
- data.tar.gz: 0a48e91fafd9a6116e47b9c0e2c6fc411f915930669ad6b182e55b37fe7e26bf
3
+ metadata.gz: 87724ea9011349f5f7c008a360728975567903827b5cdcf9a42e2678ca84b134
4
+ data.tar.gz: ae10f48baf9522bd7087a20e1886fc26170c1f5e0a0c36a3ccac26c836f11885
5
5
  SHA512:
6
- metadata.gz: e781def1a51ef817fb607300bd5439844289ad8d8a77de4ef103d6187c01a4681ee1cf2db917b03adf3df7898b6ab05b4f0a7330631fee6d4e5fa54a3054f805
7
- data.tar.gz: 1f392189d7e2b42da4bfcf1d92d980e2b813b68fb6c88e18c275b57afdf5cb741d84311fd5092f8552ef7c18f62f660e2a197f79cfd8c7bcde709fe474aa368e
6
+ metadata.gz: b21c5413da8dfe9f60d9845a578bcfe8337982d128a5041df214004e144b9913dd34cd2da1514a30ea67e21d52604a11218c362a46f9c990c472ac4ec14a0fa9
7
+ data.tar.gz: ae07a4aba39ce05f20b6a495310b6a20c1c468e0d627393ab931af76d9cd57da5b52075a33706a8b94fadae01c32b65d818d4f5c1018f9eac08a8ffdc10c7ace
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.18] - 2026-04-24
4
+
5
+ ### Fixed
6
+ - `store_knowledge`, `query_knowledge`, `related_entries` now execute directly when PostgreSQL is available instead of returning unexecuted command hashes — MCP tool calls were returning dispatch payloads (`{action: :query, ...}`) as their result, making the knowledge base unsearchable via LLM tools
7
+ - `query_knowledge` and `retrieve_relevant` now include `candidate` status in default search filters so newly stored entries are immediately retrievable (previously only `confirmed` entries were returned)
8
+
9
+ ### Changed
10
+ - Reduce `POWER_LAW_ALPHA` from 0.5 to 0.05 — decay was compounding hourly and crushing entry confidence within a day
11
+ - Reduce `DECAY_THRESHOLD` from 0.1 to 0.05 — entries were being archived too eagerly
12
+ - Decay cycle now operates on age in **days** instead of hours, preventing aggressive per-cycle compounding
13
+ - Add `DECAY_MIN_AGE_HOURS` (168h / 7 days) — entries younger than this are completely exempt from decay and archival
14
+ - `apply_decay` Ruby helper matches the new SQL behavior (days-based, minimum age guard)
15
+
16
+ ## [0.4.17] - 2026-04-03
17
+
18
+ ### Changed
19
+ - resolve rubocop Style/IfUnlessModifier offense in runners/knowledge.rb
20
+
3
21
  ## [0.4.16] - 2026-03-30
4
22
 
5
23
  ### Changed
@@ -8,12 +8,13 @@ module Legion
8
8
  INITIAL_CONFIDENCE = 0.5
9
9
  CORROBORATION_BOOST = 0.3
10
10
  RETRIEVAL_BOOST = 0.02
11
- POWER_LAW_ALPHA = 0.5
12
- DECAY_THRESHOLD = 0.1
11
+ POWER_LAW_ALPHA = 0.05
12
+ DECAY_THRESHOLD = 0.05
13
13
  CORROBORATION_SIMILARITY_THRESHOLD = 0.9
14
14
  WRITE_CONFIDENCE_GATE = 0.6
15
15
  WRITE_NOVELTY_GATE = 0.3
16
16
  STALE_DAYS = 90
17
+ DECAY_MIN_AGE_HOURS = 168
17
18
  CONTENT_TYPES = %i[fact concept procedure association observation].freeze
18
19
  STATUSES = %w[candidate confirmed disputed decayed archived].freeze
19
20
  RELATION_TYPES = %w[is_a has_a part_of causes similar_to contradicts supersedes depends_on].freeze
@@ -41,14 +42,18 @@ module Legion
41
42
  def write_confidence_gate = apollo_setting(:confidence, :write_gate, default: WRITE_CONFIDENCE_GATE)
42
43
  def write_novelty_gate = apollo_setting(:confidence, :novelty_gate, default: WRITE_NOVELTY_GATE)
43
44
  def stale_days = apollo_setting(:stale_days, default: STALE_DAYS)
45
+ def decay_min_age_hours = apollo_setting(:decay_min_age_hours, default: DECAY_MIN_AGE_HOURS)
44
46
 
45
47
  def corroboration_similarity_threshold
46
48
  apollo_setting(:confidence, :corroboration_similarity, default: CORROBORATION_SIMILARITY_THRESHOLD)
47
49
  end
48
50
 
49
51
  def apply_decay(confidence:, age_hours: nil, alpha: power_law_alpha, **)
52
+ return confidence if age_hours && age_hours < decay_min_age_hours
53
+
50
54
  if age_hours
51
- [confidence * ((age_hours.clamp(0, Float::INFINITY) + 2.0)**(-alpha)) / ((age_hours.clamp(0, Float::INFINITY) + 1.0)**(-alpha)), 0.0].max
55
+ age_days = age_hours / 24.0
56
+ [confidence * ((age_days.clamp(1, Float::INFINITY) + 1.0)**(-alpha)) / (age_days.clamp(1, Float::INFINITY)**(-alpha)), 0.0].max
52
57
  else
53
58
  factor = 1.0 / (1.0 + alpha)
54
59
  [confidence * factor, 0.0].max
@@ -20,6 +20,11 @@ module Legion
20
20
  raise ArgumentError, "invalid content_type: #{content_type}. Must be one of #{Helpers::Confidence::CONTENT_TYPES}"
21
21
  end
22
22
 
23
+ if defined?(Legion::Data::Model::ApolloEntry)
24
+ return handle_ingest(content: content, content_type: content_type,
25
+ tags: Array(tags), source_agent: source_agent, context: context, **)
26
+ end
27
+
23
28
  {
24
29
  action: :store,
25
30
  content: content,
@@ -30,7 +35,12 @@ module Legion
30
35
  }
31
36
  end
32
37
 
33
- def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: [:confirmed], tags: nil, **) # rubocop:disable Layout/LineLength
38
+ def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: %i[confirmed candidate], tags: nil, **) # rubocop:disable Layout/LineLength
39
+ if defined?(Legion::Data::Model::ApolloEntry)
40
+ return handle_query(query: query, limit: limit, min_confidence: min_confidence,
41
+ status: status, tags: tags, **)
42
+ end
43
+
34
44
  {
35
45
  action: :query,
36
46
  query: query,
@@ -42,6 +52,8 @@ module Legion
42
52
  end
43
53
 
44
54
  def related_entries(entry_id:, relation_types: nil, depth: Helpers::GraphQuery.default_depth, **)
55
+ return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if defined?(Legion::Data::Model::ApolloEntry)
56
+
45
57
  {
46
58
  action: :traverse,
47
59
  entry_id: entry_id,
@@ -121,6 +133,7 @@ module Legion
121
133
  def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: [:confirmed], tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength
122
134
  return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
123
135
 
136
+ query = normalize_text_input(query)
124
137
  embedding = embed_text(query)
125
138
  sql = Helpers::GraphQuery.build_semantic_search_sql(
126
139
  limit: limit, min_confidence: min_confidence,
@@ -228,12 +241,13 @@ module Legion
228
241
 
229
242
  return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
230
243
 
244
+ query = normalize_text_input(query)
231
245
  return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
232
246
 
233
- embedding = embed_text(query.to_s)
247
+ embedding = embed_text(query)
234
248
  sql = Helpers::GraphQuery.build_semantic_search_sql(
235
249
  limit: limit, min_confidence: min_confidence,
236
- statuses: ['confirmed'], tags: tags, domain: domain
250
+ statuses: %w[confirmed candidate], tags: tags, domain: domain
237
251
  )
238
252
 
239
253
  db = Legion::Data::Model::ApolloEntry.db
@@ -313,6 +327,7 @@ module Legion
313
327
  private
314
328
 
315
329
  def embed_text(text)
330
+ text = normalize_text_input(text)
316
331
  result = Legion::LLM::Embeddings.generate(text: text)
317
332
  vector = result.is_a?(Hash) ? result[:vector] : result
318
333
  vector.is_a?(Array) && vector.any? ? vector : Array.new(1024, 0.0)
@@ -321,6 +336,15 @@ module Legion
321
336
  Array.new(1024, 0.0)
322
337
  end
323
338
 
339
+ def normalize_text_input(value)
340
+ return Legion::Apollo.send(:normalize_text_input, value) if defined?(Legion::Apollo) && Legion::Apollo.respond_to?(:normalize_text_input, true)
341
+
342
+ value.to_s
343
+ rescue StandardError => e
344
+ log.warn("Apollo Knowledge.normalize_text_input failed: #{e.message}")
345
+ value.to_s
346
+ end
347
+
324
348
  def allowed_domains_for(target_domain)
325
349
  rules = if defined?(Legion::Settings) && Legion::Settings.dig(:apollo, :domain_isolation)
326
350
  Legion::Settings.dig(:apollo, :domain_isolation)
@@ -23,28 +23,37 @@ module Legion
23
23
  def run_decay_cycle(alpha: nil, min_confidence: nil, **)
24
24
  alpha ||= Helpers::Confidence.power_law_alpha
25
25
  min_confidence ||= Helpers::Confidence.decay_threshold
26
+ min_age_hours = Helpers::Confidence.decay_min_age_hours
26
27
 
27
28
  return { decayed: 0, archived: 0 } unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
28
29
 
29
30
  conn = Legion::Data.connection
30
31
 
31
- hours_expr = Sequel.lit(
32
- 'GREATEST(EXTRACT(EPOCH FROM (NOW() - COALESCE(updated_at, created_at))) / 3600.0, 1.0)'
32
+ age_days_expr = Sequel.lit(
33
+ 'GREATEST(EXTRACT(EPOCH FROM (NOW() - COALESCE(updated_at, created_at))) / 86400.0, 1.0)'
33
34
  )
34
35
  decay_factor = Sequel.lit(
35
- 'POWER(CAST(? AS double precision) / (CAST(? AS double precision) + 1.0), ?)', hours_expr, hours_expr, alpha
36
+ 'POWER(CAST(? AS double precision) / (CAST(? AS double precision) + 1.0), ?)',
37
+ age_days_expr, age_days_expr, alpha
38
+ )
39
+
40
+ min_age_filter = Sequel.lit(
41
+ "COALESCE(updated_at, created_at) < NOW() - INTERVAL '? hours'", min_age_hours
36
42
  )
37
43
 
38
44
  decayed = conn[:apollo_entries]
39
45
  .exclude(status: 'archived')
46
+ .where(min_age_filter)
40
47
  .update(confidence: Sequel[:confidence] * decay_factor)
41
48
 
42
49
  archived = conn[:apollo_entries]
43
50
  .where { confidence < min_confidence }
51
+ .where(min_age_filter)
44
52
  .exclude(status: 'archived')
45
53
  .update(status: 'archived')
46
54
 
47
- { decayed: decayed, archived: archived, alpha: alpha, threshold: min_confidence }
55
+ { decayed: decayed, archived: archived, alpha: alpha, threshold: min_confidence,
56
+ min_age_hours: min_age_hours }
48
57
  rescue Sequel::Error => e
49
58
  { decayed: 0, archived: 0, error: e.message }
50
59
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.16'
6
+ VERSION = '0.4.18'
7
7
  end
8
8
  end
9
9
  end
@@ -18,11 +18,15 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
18
18
  end
19
19
 
20
20
  it 'defines POWER_LAW_ALPHA' do
21
- expect(described_class::POWER_LAW_ALPHA).to eq(0.5)
21
+ expect(described_class::POWER_LAW_ALPHA).to eq(0.05)
22
22
  end
23
23
 
24
24
  it 'defines DECAY_THRESHOLD' do
25
- expect(described_class::DECAY_THRESHOLD).to eq(0.1)
25
+ expect(described_class::DECAY_THRESHOLD).to eq(0.05)
26
+ end
27
+
28
+ it 'defines DECAY_MIN_AGE_HOURS' do
29
+ expect(described_class::DECAY_MIN_AGE_HOURS).to eq(168)
26
30
  end
27
31
 
28
32
  it 'defines CORROBORATION_SIMILARITY_THRESHOLD' do
@@ -45,12 +49,17 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
45
49
  describe '.apply_decay' do
46
50
  it 'applies power-law decay with default alpha when no age given' do
47
51
  result = described_class.apply_decay(confidence: 1.0)
48
- expected = 1.0 / (1.0 + 0.5) # ~0.6667
52
+ expected = 1.0 / (1.0 + 0.05) # ~0.9524
49
53
  expect(result).to be_within(0.0001).of(expected)
50
54
  end
51
55
 
52
- it 'applies age-based power-law decay when age_hours is provided' do
56
+ it 'skips decay when age_hours is below minimum age' do
53
57
  result = described_class.apply_decay(confidence: 1.0, age_hours: 10)
58
+ expect(result).to eq(1.0)
59
+ end
60
+
61
+ it 'applies age-based power-law decay when age_hours exceeds minimum' do
62
+ result = described_class.apply_decay(confidence: 1.0, age_hours: 500)
54
63
  expect(result).to be > 0.0
55
64
  expect(result).to be < 1.0
56
65
  end
@@ -98,11 +107,11 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
98
107
 
99
108
  describe '.decayed?' do
100
109
  it 'returns true when confidence below threshold' do
101
- expect(described_class.decayed?(confidence: 0.05)).to be true
110
+ expect(described_class.decayed?(confidence: 0.01)).to be true
102
111
  end
103
112
 
104
113
  it 'returns false when confidence at or above threshold' do
105
- expect(described_class.decayed?(confidence: 0.1)).to be false
114
+ expect(described_class.decayed?(confidence: 0.05)).to be false
106
115
  end
107
116
  end
108
117
 
@@ -14,11 +14,15 @@ RSpec.describe 'Apollo Decay Cycle' do
14
14
 
15
15
  describe 'configurable decay parameters' do
16
16
  it 'returns POWER_LAW_ALPHA as default' do
17
- expect(Legion::Extensions::Apollo::Helpers::Confidence.power_law_alpha).to eq(0.5)
17
+ expect(Legion::Extensions::Apollo::Helpers::Confidence.power_law_alpha).to eq(0.05)
18
18
  end
19
19
 
20
20
  it 'returns default decay threshold' do
21
- expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_threshold).to eq(0.1)
21
+ expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_threshold).to eq(0.05)
22
+ end
23
+
24
+ it 'returns default decay minimum age hours' do
25
+ expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_min_age_hours).to eq(168)
22
26
  end
23
27
  end
24
28
  end
@@ -71,7 +71,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Maintenance do
71
71
 
72
72
  it 'returns alpha in result hash' do
73
73
  result = host.run_decay_cycle
74
- expect(result[:alpha]).to eq(0.5)
74
+ expect(result[:alpha]).to eq(0.05)
75
75
  expect(result).not_to have_key(:rate)
76
76
  end
77
77
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-apollo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.16
4
+ version: 0.4.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -125,30 +125,30 @@ dependencies:
125
125
  name: rubocop
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - "~>"
128
+ - - ">="
129
129
  - !ruby/object:Gem::Version
130
- version: '1.0'
130
+ version: '0'
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - "~>"
135
+ - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: '1.0'
137
+ version: '0'
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: rubocop-legion
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
- - - "~>"
142
+ - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: '0.1'
144
+ version: '0'
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
- - - "~>"
149
+ - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: '0.1'
151
+ version: '0'
152
152
  description: Durable shared knowledge with vector search, concept graph, and expertise
153
153
  tracking
154
154
  email: