legion-data 1.8.8 → 1.8.9
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/CLAUDE.md +128 -15
- data/lib/legion/data/migrations/116_make_tool_calls_response_id_nullable.rb +30 -0
- data/lib/legion/data/migrations/117_add_conversation_id_to_llm_tool_calls.rb +17 -0
- data/lib/legion/data/models/llm/tool_call.rb +1 -0
- data/lib/legion/data/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18c9b2db6f7648fbad03b852d4565fe1c0ad0f46173a2130a6184d4b47b59d8a
|
|
4
|
+
data.tar.gz: 2b31d060c165c7656967642d80760ff7e94968f2d069633f9b7b407ea19f8808
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acce9df23ea4557560b5fc8b18bbb5a06acef0cdefbe75d9b9168371043575ffd8285b069b83b69d4b17f898f349702a47f780ebcd8de44fa9fd2dbdd60ff0a0
|
|
7
|
+
data.tar.gz: 5f4c6c236e41ff258f5f2489435b0d6f76072e764d242f91d0c8e735ef6b369daa778fc3f1fcccf51192f2d9ad4006702e86be68354b0b84510eba60767e5471
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Legion::Data Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.9] - 2026-05-26
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Migration 116: make `llm_tool_calls.message_inference_response_id` nullable and drop composite unique index on `[message_inference_response_id, tool_call_index]`. Eliminates 30-40% dead-letter rate on tool audit messages caused by AMQP race between response and tool call writers.
|
|
7
|
+
- Migration 117: add nullable `conversation_id` FK to `llm_tool_calls` referencing `llm_conversations`, so tool call rows can track their conversation even when the response row hasn't been written yet.
|
|
8
|
+
- Add `many_to_one :conversation` association to `LLM::ToolCall` model.
|
|
9
|
+
|
|
3
10
|
## [1.8.8] - 2026-05-20
|
|
4
11
|
|
|
5
12
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -32,11 +32,116 @@ jq '[.examples[] | select(.status != "passed") | {file_path, line_number, full_d
|
|
|
32
32
|
|
|
33
33
|
- Never edit published migrations. Add a new migration.
|
|
34
34
|
- Do not guard migrations with `create_table?`, `drop_table?`, `table_exists?`, `if_exists`, `if_not_exists`, `next if`, or `next unless`.
|
|
35
|
-
-
|
|
35
|
+
- **One change per migration file.** Each migration modifies exactly ONE table. Never loop over tables. If a migration fails, you must be able to identify exactly what broke and roll back cleanly.
|
|
36
|
+
- Never use `.each`, `.map`, or any iterator in a migration. If 12 tables need the same column, that's 12 migration files.
|
|
37
|
+
- Never use raw SQL (`run '...'`) when Sequel DSL supports the operation. Use `add_index`, `drop_index`, `add_column`, `drop_column`, etc.
|
|
36
38
|
- Use portable Sequel DSL unless the feature truly requires adapter-specific behavior.
|
|
37
39
|
- Use integer `id` primary keys for joins and public `uuid` columns for APIs/logs/external references.
|
|
38
40
|
- Normalize stable fields. Use JSON only for genuinely dynamic provider payloads or evidence.
|
|
39
41
|
|
|
42
|
+
### Sequel Migration DSL Reference
|
|
43
|
+
|
|
44
|
+
**Create table**: https://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html#method-i-create_table
|
|
45
|
+
**Column options**: https://sequel.jeremyevans.net/rdoc/classes/Sequel/Schema/CreateTableGenerator.html#method-i-column
|
|
46
|
+
|
|
47
|
+
### Create Table Pattern
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# frozen_string_literal: true
|
|
51
|
+
|
|
52
|
+
Sequel.migration do
|
|
53
|
+
change do
|
|
54
|
+
create_table(:example_records) do
|
|
55
|
+
primary_key :id
|
|
56
|
+
String :uuid, size: 36, null: false, unique: true
|
|
57
|
+
|
|
58
|
+
# Identity columns (required on every table)
|
|
59
|
+
String :access_scope, size: 20, null: false, default: 'global', index: true
|
|
60
|
+
foreign_key :identity_principal_id, :identity_principals, null: true, on_delete: :set_null, on_update: :cascade
|
|
61
|
+
foreign_key :identity_id, :identities, null: true, on_delete: :set_null, on_update: :cascade
|
|
62
|
+
String :identity_canonical_name, size: 255, null: true, index: true
|
|
63
|
+
|
|
64
|
+
# Domain columns here...
|
|
65
|
+
|
|
66
|
+
# Timestamps (required on every table)
|
|
67
|
+
DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP # reflects when the event happened (request/AMQP timestamp)
|
|
68
|
+
DateTime :inserted_at, null: false, default: Sequel::CURRENT_TIMESTAMP # when the row was physically written to the database
|
|
69
|
+
DateTime :updated_at, null: true # set on row update; NULL means never updated
|
|
70
|
+
|
|
71
|
+
index :identity_principal_id
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Alter Table Pattern (adding a column)
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# frozen_string_literal: true
|
|
81
|
+
|
|
82
|
+
Sequel.migration do
|
|
83
|
+
up do
|
|
84
|
+
alter_table(:target_table) do
|
|
85
|
+
add_column :new_column, String, size: 128, null: true, index: true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
down do
|
|
90
|
+
alter_table(:target_table) do
|
|
91
|
+
drop_index :new_column
|
|
92
|
+
drop_column :new_column
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Column Option Reference
|
|
99
|
+
|
|
100
|
+
| Option | Purpose |
|
|
101
|
+
|--------|---------|
|
|
102
|
+
| `:null` | `false` = NOT NULL, `true` = nullable |
|
|
103
|
+
| `:default` | Default value (use `Sequel::CURRENT_TIMESTAMP` for timestamps) |
|
|
104
|
+
| `:index` | `true` creates an index on this column; pass a Hash for index options |
|
|
105
|
+
| `:unique` | `true` adds a UNIQUE constraint |
|
|
106
|
+
| `:on_delete` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
|
|
107
|
+
| `:on_update` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
|
|
108
|
+
| `:key` | For FKs — the referenced column (unnecessary if referencing primary key) |
|
|
109
|
+
| `:size` | Column width for String/Decimal |
|
|
110
|
+
| `:text` | `true` for TEXT columns (unlimited length) |
|
|
111
|
+
|
|
112
|
+
### Foreign Key Conventions
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# FK to identity tables — always cascade updates, set null on delete
|
|
116
|
+
foreign_key :identity_principal_id, :identity_principals, null: true, on_delete: :set_null, on_update: :cascade
|
|
117
|
+
foreign_key :identity_id, :identities, null: true, on_delete: :set_null, on_update: :cascade
|
|
118
|
+
|
|
119
|
+
# FK to domain tables — cascade delete (child dies with parent)
|
|
120
|
+
foreign_key :conversation_id, :llm_conversations, null: false, on_delete: :cascade
|
|
121
|
+
|
|
122
|
+
# FK to optional parent — set null on delete (orphan is ok)
|
|
123
|
+
foreign_key :parent_message_id, :llm_messages, null: true, on_delete: :set_null
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Timestamp Semantics
|
|
127
|
+
|
|
128
|
+
| Column | Meaning | Default | Nullable |
|
|
129
|
+
|--------|---------|---------|----------|
|
|
130
|
+
| `created_at` | When the event/action occurred in the real world (e.g. AMQP message timestamp, API request time) | `CURRENT_TIMESTAMP` | NOT NULL |
|
|
131
|
+
| `inserted_at` | When the row was physically written to this database — always DB clock time | `CURRENT_TIMESTAMP` | NOT NULL |
|
|
132
|
+
| `updated_at` | Last time the row was modified after initial insert. NULL means never updated. | none | NULL |
|
|
133
|
+
|
|
134
|
+
`created_at` vs `inserted_at`: a message published at 14:00:00 that gets consumed and written at 14:00:03 has `created_at = 14:00:00` and `inserted_at = 14:00:03`. For synchronous writes they will be the same.
|
|
135
|
+
|
|
136
|
+
### Index Conventions
|
|
137
|
+
|
|
138
|
+
- `access_scope` — always indexed (high cardinality filter for multi-tenant queries)
|
|
139
|
+
- `identity_canonical_name` — always indexed (user-facing search/filter)
|
|
140
|
+
- `identity_principal_id` — always indexed (join path to identity tables)
|
|
141
|
+
- `uuid` — always unique index (external reference lookups)
|
|
142
|
+
- Timestamp columns used in WHERE clauses — indexed
|
|
143
|
+
- Composite indexes for common query patterns: `index [:provider, :model_key]`
|
|
144
|
+
|
|
40
145
|
## Sequel ORM Rules
|
|
41
146
|
|
|
42
147
|
Use Sequel associations as the object graph. References:
|
|
@@ -55,26 +160,30 @@ When Sequel cannot infer names, set `:class`, `:key`, `:primary_key`, `:join_tab
|
|
|
55
160
|
|
|
56
161
|
All new tables in legion-data should follow this column convention. Required fields must be present on every table. Optional fields are added when the domain warrants them.
|
|
57
162
|
|
|
58
|
-
### Required
|
|
163
|
+
### Required (every table, in this order)
|
|
59
164
|
|
|
60
|
-
| Column |
|
|
61
|
-
|
|
62
|
-
| `id` | `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
165
|
+
| Column | Sequel DSL | Purpose |
|
|
166
|
+
|--------|-----------|---------|
|
|
167
|
+
| `id` | `primary_key :id` | Auto-increment integer PK — internal join key, never exposed externally |
|
|
168
|
+
| `uuid` | `String :uuid, size: 36, null: false, unique: true` | External reference — used in APIs, logs, AMQP correlation |
|
|
169
|
+
| `access_scope` | `String :access_scope, size: 20, null: false, default: 'global', index: true` | Multi-tenant scoping (global, personal, team, org) |
|
|
170
|
+
| `identity_principal_id` | `foreign_key :identity_principal_id, :identity_principals, null: true, on_delete: :set_null, on_update: :cascade` | FK to the principal who caused this row |
|
|
171
|
+
| `identity_id` | `foreign_key :identity_id, :identities, null: true, on_delete: :set_null, on_update: :cascade` | FK to the specific provider-bound identity credential |
|
|
172
|
+
| `identity_canonical_name` | `String :identity_canonical_name, size: 255, null: true, index: true` | Point-in-time snapshot of the identity's canonical name. NOT a FK. May become stale if principal is renamed — use FK join for authoritative lookups. |
|
|
173
|
+
| `created_at` | `DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP` | When the event/action occurred (AMQP timestamp, request time) |
|
|
174
|
+
| `inserted_at` | `DateTime :inserted_at, null: false, default: Sequel::CURRENT_TIMESTAMP` | When the row was physically written to the database |
|
|
175
|
+
| `updated_at` | `DateTime :updated_at, null: true` | Set on row update; NULL means never updated after insert |
|
|
68
176
|
|
|
69
177
|
### Optional (add when applicable)
|
|
70
178
|
|
|
71
179
|
| Column | Type | Purpose |
|
|
72
180
|
|--------|------|---------|
|
|
73
|
-
| `expires_at` | `
|
|
74
|
-
| `content_type` | `
|
|
75
|
-
| `conversation_id` | `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
181
|
+
| `expires_at` | `DateTime, null: true` | TTL / archival eligibility |
|
|
182
|
+
| `content_type` | `String, size: 64` | Classifier for the row's payload kind |
|
|
183
|
+
| `conversation_id` | `foreign_key ..., :llm_conversations, on_delete: :cascade` | Links to the LLM conversation that produced this row |
|
|
184
|
+
| `task_id` | `foreign_key ..., :tasks, on_delete: :set_null` | Links to the task that triggered this row |
|
|
185
|
+
| `contains_phi` | `TrueClass, default: false` | Row contains Protected Health Information |
|
|
186
|
+
| `contains_pii` | `TrueClass, default: false` | Row contains Personally Identifiable Information |
|
|
78
187
|
|
|
79
188
|
### Naming rules
|
|
80
189
|
|
|
@@ -87,6 +196,10 @@ All new tables in legion-data should follow this column convention. Required fie
|
|
|
87
196
|
- `074`-`076`: Apollo field width, task idempotency, extract step timings.
|
|
88
197
|
- `077`-`090`: LLM lifecycle ledger.
|
|
89
198
|
- `091`-`096`: portable identity companion tables.
|
|
199
|
+
- `097`: LLM dispatch fields (operation, correlation_id, provider_instance, dispatch_path).
|
|
200
|
+
- `098`-`099`: Legacy identity table drop + rename (portable_identity_* → identity_*).
|
|
201
|
+
- `100`-`102`: Apollo identity columns + access_scope + indexes.
|
|
202
|
+
- `103`-`114`: LLM table identity standardization (access_scope, identity_principal_id, identity_id, identity_canonical_name).
|
|
90
203
|
- Namespaced models: `Identity::*`, `Apollo::*`, `RBAC::*`, `LLM::*`.
|
|
91
204
|
|
|
92
205
|
## Boundaries
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
alter_table(:llm_tool_calls) do
|
|
6
|
+
set_column_allow_null :message_inference_response_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# SQLite's set_column_allow_null recreates the table internally, which
|
|
10
|
+
# drops partial indexes invisible to Sequel's indexes() method. Restore
|
|
11
|
+
# the partial index from migration 109 (no-op on PG where it survives).
|
|
12
|
+
alter_table(:llm_tool_calls) do
|
|
13
|
+
add_index :identity_principal_id, name: :idx_tool_calls_identity_principal_id,
|
|
14
|
+
where: Sequel.negate(identity_principal_id: nil),
|
|
15
|
+
ignore_errors: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
down do
|
|
20
|
+
alter_table(:llm_tool_calls) do
|
|
21
|
+
set_column_not_null :message_inference_response_id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alter_table(:llm_tool_calls) do
|
|
25
|
+
add_index :identity_principal_id, name: :idx_tool_calls_identity_principal_id,
|
|
26
|
+
where: Sequel.negate(identity_principal_id: nil),
|
|
27
|
+
ignore_errors: true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
alter_table(:llm_tool_calls) do
|
|
6
|
+
add_foreign_key :conversation_id, :llm_conversations, null: true, on_delete: :set_null, on_update: :cascade
|
|
7
|
+
add_index :conversation_id
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
down do
|
|
12
|
+
alter_table(:llm_tool_calls) do
|
|
13
|
+
drop_index :conversation_id
|
|
14
|
+
drop_foreign_key :conversation_id
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -10,6 +10,7 @@ module Legion
|
|
|
10
10
|
include ModelHelpers
|
|
11
11
|
|
|
12
12
|
many_to_one :message_inference_response
|
|
13
|
+
many_to_one :conversation
|
|
13
14
|
many_to_one :requested_by_message, class: 'Legion::Data::Models::LLM::Message', key: :requested_by_message_id
|
|
14
15
|
many_to_one :result_message, class: 'Legion::Data::Models::LLM::Message', key: :result_message_id
|
|
15
16
|
one_to_many :tool_call_attempts
|
data/lib/legion/data/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.8.
|
|
4
|
+
version: 1.8.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -262,6 +262,8 @@ files:
|
|
|
262
262
|
- lib/legion/data/migrations/113_add_llm_security_events_identity_columns.rb
|
|
263
263
|
- lib/legion/data/migrations/114_add_llm_registry_events_identity_columns.rb
|
|
264
264
|
- lib/legion/data/migrations/115_add_runtime_caller_columns.rb
|
|
265
|
+
- lib/legion/data/migrations/116_make_tool_calls_response_id_nullable.rb
|
|
266
|
+
- lib/legion/data/migrations/117_add_conversation_id_to_llm_tool_calls.rb
|
|
265
267
|
- lib/legion/data/model.rb
|
|
266
268
|
- lib/legion/data/models/apollo/access_log.rb
|
|
267
269
|
- lib/legion/data/models/apollo/entries.rb
|