Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ Naming/VariableNumber:
- lib/legion/data/connection.rb
Legion/Framework/EagerSequelModel:
Enabled: false
# TaxonomyEnum validates LLM lane taxonomy (:tier/:type/:circuit_state). In
# these paths `type:` is a Sequel column type (e.g. foreign_key type: :uuid) or
# the extract API (type: :auto/:text) — not LLM taxonomy — so the cop misfires.
Legion/Llm/TaxonomyEnum:
Exclude:
- 'lib/legion/data/migrations/**/*'
- 'spec/**/*'
Metrics/BlockLength:
Max: 100
Exclude:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Legion::Data Changelog

## [1.10.6] - 2026-07-03

### Changed
- Migration 136: drops the `(conversation_id, seq)` UNIQUE constraint on `llm_messages`, replacing it with a NON-unique composite index. A conversation is a tree, not a line — a single parent can legitimately have two children at the same depth (e.g. a failed branch and the branch that continued), so two distinct messages can share `(conversation_id, seq)` and that is truth, not a duplicate. The unique constraint rejected legitimate siblings and forced the centralized ledger into a racy `MAX(seq)+1` generation that collided under concurrent writers (`PG::UniqueViolation` on `llm_messages_conversation_id_seq_key`). Message identity remains guaranteed by the unique `uuid`; `seq` becomes descriptive tree-depth. On SQLite the constraint drop rebuilds the table (dropping the inline `uuid` UNIQUE), so the migration re-asserts the uuid unique index — a no-op on PostgreSQL where `DROP CONSTRAINT` is surgical.

## [1.10.5] - 2026-06-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion lib/legion/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def stats
def connected?
Legion::Settings[:data][:connected] == true
rescue StandardError => e
handle_exception(e, level: :debug, handled: true, operation: :connected?)
handle_exception(e, level: :warn, handled: true, operation: :connected?)
false
end

Expand Down
21 changes: 9 additions & 12 deletions lib/legion/data/archiver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,15 @@ def archive_batches(conn:, table:, cutoff:, batch_size:, storage_backend:)
total_rows = 0
paths = []

loop do
batch_result = archive_batch(
conn: conn,
table: table,
cutoff: cutoff,
batch_size: batch_size,
batch_n: batches + 1,
now: now,
storage_backend: storage_backend
)
break unless batch_result

while (batch_result = archive_batch(
conn: conn,
table: table,
cutoff: cutoff,
batch_size: batch_size,
batch_n: batches + 1,
now: now,
storage_backend: storage_backend
))
batches += 1
total_rows += batch_result[:row_count]
paths << batch_result[:path]
Expand Down
2 changes: 1 addition & 1 deletion lib/legion/data/extract/handlers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def available?
require gem_name
true
rescue LoadError => e
handle_exception(e, level: :debug, handled: true, operation: :extract_handler_available, handler: name, gem: gem_name)
handle_exception(e, level: :warn, handled: true, operation: :extract_handler_available, handler: name, gem: gem_name)
false
end
end
Expand Down
36 changes: 36 additions & 0 deletions lib/legion/data/migrations/136_drop_llm_messages_seq_unique.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

# The (conversation_id, seq) UNIQUE constraint modeled a conversation as a flat
# line, but a conversation is a tree: a single parent can legitimately have two
# children at the same depth (e.g. a failed branch and the branch that
# succeeded), so two distinct messages can share the same (conversation_id, seq)
# and that is TRUTH, not a duplicate. The unique constraint rejected those
# legitimate siblings and forced the ledger into a racy MAX(seq)+1 generation
# that collided under concurrent writers.
#
# Message identity is guaranteed by the unique `uuid` (producer-derived, stable
# across redelivery). seq/parent become descriptive ordering data, so we keep a
# NON-unique composite index for conversation-ordered reads.
Sequel.migration do
up do
alter_table(:llm_messages) do
drop_constraint(:llm_messages_conversation_id_seq_key, type: :unique)
add_index %i[conversation_id seq], name: :idx_llm_messages_conversation_seq
end
# SQLite emulates the constraint drop by rebuilding the table, which drops
# the inline `uuid` UNIQUE. Re-assert it so message identity/dedup is never
# lost. On PostgreSQL DROP CONSTRAINT is surgical and the original uuid index
# already exists, so this is a no-op there.
alter_table(:llm_messages) do
add_index :uuid, unique: true, name: :idx_llm_messages_uuid_unique, if_not_exists: true
end
end

down do
alter_table(:llm_messages) do
drop_index :uuid, name: :idx_llm_messages_uuid_unique, if_exists: true
drop_index %i[conversation_id seq], name: :idx_llm_messages_conversation_seq
add_unique_constraint %i[conversation_id seq], name: :llm_messages_conversation_id_seq_key
end
end
end
2 changes: 1 addition & 1 deletion lib/legion/data/models/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def embedding_vector

::JSON.parse(embedding)
rescue ::JSON::ParserError => e
handle_exception(e, level: :debug, handled: true, operation: :embedding_vector, id: self[:id])
handle_exception(e, level: :warn, handled: true, operation: :embedding_vector, id: self[:id])
nil
end

Expand Down
4 changes: 2 additions & 2 deletions lib/legion/data/models/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
def parsed_metrics
return nil unless metrics

Legion::JSON.load(metrics)

Check notice on line 18 in lib/legion/data/models/node.rb

View workflow job for this annotation

GitHub Actions / lint / Helper Migration

helper-json-direct

In runner/actor code, prefer `json_load(str)` / `json_dump(obj)` helpers. Direct calls are for framework code only.
rescue StandardError => e
handle_exception(e, level: :debug, handled: true, operation: :parsed_metrics, id: self[:id])
handle_exception(e, level: :warn, handled: true, operation: :parsed_metrics, id: self[:id])
nil
end

def parsed_hosted_worker_ids
return [] unless hosted_worker_ids

Legion::JSON.load(hosted_worker_ids)

Check notice on line 27 in lib/legion/data/models/node.rb

View workflow job for this annotation

GitHub Actions / lint / Helper Migration

helper-json-direct

In runner/actor code, prefer `json_load(str)` / `json_dump(obj)` helpers. Direct calls are for framework code only.
rescue StandardError => e
handle_exception(e, level: :debug, handled: true, operation: :parsed_hosted_worker_ids, id: self[:id])
handle_exception(e, level: :warn, handled: true, operation: :parsed_hosted_worker_ids, id: self[:id])
[]
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/legion/data/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Legion
module Data
VERSION = '1.10.5'
VERSION = '1.10.6'
end
end
2 changes: 1 addition & 1 deletion spec/legion/data/connection_replicas_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
end

# Build a minimal Sequel::Database double with the methods we call.
def fake_sequel_db(**_opts)
def fake_sequel_db(**)
db = instance_double(Sequel::Database)
allow(db).to receive(:extension)
allow(db).to receive(:add_servers)
Expand Down
4 changes: 2 additions & 2 deletions spec/legion/data/tls_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

before do
stub_const('Legion::Crypt::TLS', Module.new do
def self.resolve(config, **_opts)
def self.resolve(config, **)
if config[:enabled]
{ enabled: true, verify: :peer, ca: '/etc/ssl/ca.pem', cert: nil, key: nil }
else
Expand Down Expand Up @@ -69,7 +69,7 @@ def self.resolve(config, **_opts)
)

stub_const('Legion::Crypt::TLS', Module.new do
def self.resolve(_config, **_opts)
def self.resolve(_config, **)
{ enabled: true, verify: :none, ca: nil, cert: nil, key: nil }
end
end)
Expand Down
Loading