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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60b8f26149494282c49455aedc216eebf0db867fa359dea9404ca44d09ad9815
4
- data.tar.gz: 4da8dfdb277d9ceda6807543f58c2804ae71f938056cff7f8bcd366c4bad9cae
3
+ metadata.gz: 18c9b2db6f7648fbad03b852d4565fe1c0ad0f46173a2130a6184d4b47b59d8a
4
+ data.tar.gz: 2b31d060c165c7656967642d80760ff7e94968f2d069633f9b7b407ea19f8808
5
5
  SHA512:
6
- metadata.gz: ef647d212100c659d28847879153403ac1dd42c55c0cb9b1529fd1e17d73f029a912d23988f6701e59e4b25d326b8512cd89f0291bcc52a9e492e6fd4c85b055
7
- data.tar.gz: '018844cceb163d4100cffcfa8ea1ed06b916889ca14111e9ca8398d9c3d2a045c9e9c4d73f735e8c22f681ccc834c23f732d08d68303973251c60085f5013c1b'
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
- - Keep migrations small enough to diagnose and roll back. Split by domain and dependency.
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 | Type | Purpose |
61
- |--------|------|---------|
62
- | `id` | `INTEGER PRIMARY KEY` (auto-increment) | Internal join key never exposed externally |
63
- | `identity_principal_id` | `INTEGER` FK `identity_principals.id` | The principal who caused this row to exist |
64
- | `identity_id` | `INTEGER` FK `identities.id` | The specific provider-bound identity credential |
65
- | `identity_canonical_name` | `VARCHAR(255)` | Denormalized snapshot of the identity's canonical name for fast filtering without joins. This value is a point-in-time copy — it may become stale if the principal is renamed. Use the FK join for authoritative lookups. |
66
- | `created_at` | `TIMESTAMPTZ` | Row creation time |
67
- | `updated_at` | `TIMESTAMPTZ` | Last modification time |
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` | `TIMESTAMPTZ` | TTL / archival eligibility |
74
- | `content_type` | `VARCHAR(...)` | Classifier for the row's payload kind |
75
- | `conversation_id` | `INTEGER` FK `llm_conversations.id` | Links to the LLM conversation that produced this row |
76
- | `contains_phi` | `BOOLEAN` | Row contains Protected Health Information |
77
- | `contains_pii` | `BOOLEAN` | Row contains Personally Identifiable Information |
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Data
5
- VERSION = '1.8.8'
5
+ VERSION = '1.8.9'
6
6
  end
7
7
  end
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.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