lex-llm-ledger 0.3.0 → 0.3.1

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: bedae52b0629ce6755fc321019b9c35917e8d4f8a304c7294956cbba864fa676
4
- data.tar.gz: c3f171de566906d37cb02c755f0336df1ce0f09dcdf769e84961f574e3b9d77e
3
+ metadata.gz: a69d4ccf5975f45c795e6e54ab6ed59272a269855394bbef4d1149a48bd338e7
4
+ data.tar.gz: 60bfc3f268526d0cf427ccfa90f2e230f9e0491fd88584ff685604c93faf63c4
5
5
  SHA512:
6
- metadata.gz: f8623ee921c7e0b610217925aea11e9f0f5b315b89b56d2ea239aefa9b0ac9d17b298b1f95978f784f6932ab005fa906c5c32bdd78d1f3eb47dcefb73f3762f9
7
- data.tar.gz: 278ed50a3f63394470ede944211cc0893e1c8c52bda2200209161040eba3a07a0d62aa47dbfbf37e913ecdaa8d474a4144f1961b60b3e0dcfdb3af936daf155e
6
+ metadata.gz: e166ea0dc31b758dd45d5c1595f6a72c78629b7dfd69d09d9928a699ce14f475658d9bf77f08018608bc98f394532f4c19b5887c3b2649bb1761aecbe2b4b5c8
7
+ data.tar.gz: 226be9d0592d67fcca0ff956fe0c62db56a311cc3f6bde5a16523f21801d43079d976a6d6582e2a0f1022396089555495df37ccf81ccfd56ddfd3f2344f9d27f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-05-13
4
+
5
+ ### Fixed
6
+ - Recover cleanly when concurrent ledger consumers create the same conversation, request, response, metric, or identity rows.
7
+ - Keep duplicate insert recovery inside savepoints so PostgreSQL transactions remain usable after unique constraint races.
8
+ - Remove temporary prompt runner debug output while preserving single-message subscription prefetch behavior.
9
+
3
10
  ## [0.3.0] - 2026-05-08
4
11
 
5
12
  ### Changed
@@ -11,6 +11,8 @@ module Legion
11
11
  class Metering < Legion::Extensions::Actors::Subscription
12
12
  include Helpers::SubscriptionActor
13
13
 
14
+ prefetch 1
15
+
14
16
  def runner_class = Legion::Extensions::Llm::Ledger::Runners::Metering
15
17
 
16
18
  def runner_function
@@ -11,6 +11,8 @@ module Legion
11
11
  class Prompts < Legion::Extensions::Actors::Subscription
12
12
  include Helpers::SubscriptionActor
13
13
 
14
+ prefetch 1
15
+
14
16
  def runner_class = Legion::Extensions::Llm::Ledger::Runners::Prompts
15
17
 
16
18
  def runner_function
@@ -11,6 +11,8 @@ module Legion
11
11
  class Tools < Legion::Extensions::Actors::Subscription
12
12
  include Helpers::SubscriptionActor
13
13
 
14
+ prefetch 1
15
+
14
16
  def runner_class = Legion::Extensions::Llm::Ledger::Runners::Tools
15
17
 
16
18
  def runner_function
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Ledger
7
- VERSION = '0.3.0'
7
+ VERSION = '0.3.1'
8
8
  end
9
9
  end
10
10
  end
@@ -57,20 +57,26 @@ module Legion
57
57
  existing = db[:llm_conversations].where(uuid: uuid).first
58
58
  return existing if existing
59
59
 
60
- id = insert_row(db, :llm_conversations, {
61
- uuid: uuid,
62
- title: body[:title] || body[:conversation_title],
63
- classification_level: classification_level(body),
64
- contains_phi: contains_phi?(body),
65
- contains_pii: contains_pii?(body),
66
- retention_policy: body[:retention_policy] || 'default',
67
- expires_at: body[:expires_at],
68
- recorded_at: recorded_at(body),
69
- inserted_at: Time.now.utc,
70
- created_at: Time.now.utc,
71
- updated_at: Time.now.utc
72
- }, operation: 'official_record_writer.conversation')
60
+ id = insert_with_savepoint(db, :llm_conversations, {
61
+ uuid: uuid,
62
+ title: body[:title] || body[:conversation_title],
63
+ classification_level: classification_level(body),
64
+ contains_phi: contains_phi?(body),
65
+ contains_pii: contains_pii?(body),
66
+ retention_policy: body[:retention_policy] || 'default',
67
+ expires_at: body[:expires_at],
68
+ recorded_at: recorded_at(body),
69
+ inserted_at: Time.now.utc,
70
+ created_at: Time.now.utc,
71
+ updated_at: Time.now.utc
72
+ }, operation: 'official_record_writer.conversation')
73
73
  db[:llm_conversations][id: id]
74
+ rescue Sequel::UniqueConstraintViolation => e
75
+ log.debug("[ledger] conversation collision resolved uuid=#{uuid} error=#{e.class}")
76
+ existing = db[:llm_conversations].where(uuid: uuid).first
77
+ return existing if existing
78
+
79
+ raise
74
80
  end
75
81
 
76
82
  def find_or_create_user_message(db, conversation, body)
@@ -80,20 +86,18 @@ module Legion
80
86
 
81
87
  seq = body[:message_seq] ? integer(body[:message_seq]) : next_message_seq(db, conversation)
82
88
  begin
83
- id = db.transaction(savepoint: true) do
84
- insert_row(db, :llm_messages, {
85
- uuid: uuid,
86
- conversation_id: conversation[:id],
87
- seq: seq,
88
- role: 'user',
89
- content_type: 'text',
90
- content: request_content(body),
91
- input_tokens: tokens(body)[:input_tokens],
92
- output_tokens: 0,
93
- created_at: recorded_at(body),
94
- inserted_at: Time.now.utc
95
- }, operation: 'official_record_writer.user_message')
96
- end
89
+ id = insert_with_savepoint(db, :llm_messages, {
90
+ uuid: uuid,
91
+ conversation_id: conversation[:id],
92
+ seq: seq,
93
+ role: 'user',
94
+ content_type: 'text',
95
+ content: request_content(body),
96
+ input_tokens: tokens(body)[:input_tokens],
97
+ output_tokens: 0,
98
+ created_at: recorded_at(body),
99
+ inserted_at: Time.now.utc
100
+ }, operation: 'official_record_writer.user_message')
97
101
  db[:llm_messages][id: id]
98
102
  rescue Sequel::UniqueConstraintViolation => e
99
103
  log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
@@ -112,31 +116,40 @@ module Legion
112
116
 
113
117
  operation = operation(body)
114
118
  caller_refs = caller_identity_refs(db, body)
115
- id = insert_row(db, :llm_message_inference_requests, {
116
- uuid: stable_uuid(request_id),
117
- conversation_id: conversation[:id],
118
- latest_message_id: latest_message[:id],
119
- caller_principal_id: caller_refs[:principal_id],
120
- caller_identity_id: caller_refs[:identity_id],
121
- runtime_caller_type: caller_type(body),
122
- request_ref: request_id,
123
- correlation_ref: correlation_id(body),
124
- correlation_id: correlation_id(body),
125
- exchange_ref: body[:exchange_id],
126
- request_type: operation,
127
- operation: operation,
128
- idempotency_key: body[:idempotency_key] || request_id,
129
- status: 'responded',
130
- context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
131
- request_capture_mode: 'full',
132
- request_json: json_dump(request_payload(body)),
133
- classification_level: classification_level(body),
134
- cost_center: billing(body)[:cost_center],
135
- budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
136
- requested_at: recorded_at(body),
137
- inserted_at: Time.now.utc
138
- }, operation: 'official_record_writer.inference_request')
119
+ id = insert_with_savepoint(db, :llm_message_inference_requests, {
120
+ uuid: stable_uuid(request_id),
121
+ conversation_id: conversation[:id],
122
+ latest_message_id: latest_message[:id],
123
+ caller_principal_id: caller_refs[:principal_id],
124
+ caller_identity_id: caller_refs[:identity_id],
125
+ runtime_caller_type: caller_type(body),
126
+ request_ref: request_id,
127
+ correlation_ref: correlation_id(body),
128
+ correlation_id: correlation_id(body),
129
+ exchange_ref: body[:exchange_id],
130
+ request_type: operation,
131
+ operation: operation,
132
+ idempotency_key: body[:idempotency_key] || request_id,
133
+ status: 'responded',
134
+ context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
135
+ request_capture_mode: 'full',
136
+ request_json: json_dump(request_payload(body)),
137
+ classification_level: classification_level(body),
138
+ cost_center: billing(body)[:cost_center],
139
+ budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
140
+ requested_at: recorded_at(body),
141
+ inserted_at: Time.now.utc
142
+ }, operation: 'official_record_writer.inference_request')
139
143
  db[:llm_message_inference_requests][id: id]
144
+ rescue Sequel::UniqueConstraintViolation => e
145
+ log.debug("[ledger] request collision resolved request_ref=#{request_id} error=#{e.class}")
146
+ existing = db[:llm_message_inference_requests].where(request_ref: request_id).first
147
+ if existing
148
+ enrich_request!(db, existing, body)
149
+ return existing
150
+ end
151
+
152
+ raise
140
153
  end
141
154
 
142
155
  def find_or_create_response_message(db, conversation, request, body)
@@ -147,22 +160,20 @@ module Legion
147
160
  latest = db[:llm_messages][id: request[:latest_message_id]]
148
161
  seq = (latest&.dig(:seq) || 1) + 1
149
162
  begin
150
- id = db.transaction(savepoint: true) do
151
- insert_row(db, :llm_messages, {
152
- uuid: uuid,
153
- conversation_id: conversation[:id],
154
- parent_message_id: latest&.dig(:id),
155
- message_inference_request_id: request[:id],
156
- seq: seq,
157
- role: 'assistant',
158
- content_type: 'text',
159
- content: response_content(body),
160
- input_tokens: 0,
161
- output_tokens: tokens(body)[:output_tokens],
162
- created_at: recorded_at(body),
163
- inserted_at: Time.now.utc
164
- }, operation: 'official_record_writer.response_message')
165
- end
163
+ id = insert_with_savepoint(db, :llm_messages, {
164
+ uuid: uuid,
165
+ conversation_id: conversation[:id],
166
+ parent_message_id: latest&.dig(:id),
167
+ message_inference_request_id: request[:id],
168
+ seq: seq,
169
+ role: 'assistant',
170
+ content_type: 'text',
171
+ content: response_content(body),
172
+ input_tokens: 0,
173
+ output_tokens: tokens(body)[:output_tokens],
174
+ created_at: recorded_at(body),
175
+ inserted_at: Time.now.utc
176
+ }, operation: 'official_record_writer.response_message')
166
177
  db[:llm_messages][id: id]
167
178
  rescue Sequel::UniqueConstraintViolation => e
168
179
  log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
@@ -179,28 +190,37 @@ module Legion
179
190
  return existing
180
191
  end
181
192
 
182
- id = insert_row(db, :llm_message_inference_responses, {
183
- uuid: response_uuid,
184
- message_inference_request_id: request[:id],
185
- response_message_id: response_message&.dig(:id),
186
- provider: provider(body),
187
- provider_instance: provider_instance(body),
188
- model_key: model_id(body),
189
- tier: tier(body),
190
- runner_ref: body[:worker_id] || body[:runner_ref],
191
- provider_response_ref: body[:provider_response_ref],
192
- status: body[:error] ? 'error' : 'success',
193
- finish_reason: finish_reason(body),
194
- latency_ms: integer(body[:latency_ms]),
195
- wall_clock_ms: integer(body[:wall_clock_ms]),
196
- response_capture_mode: 'full',
197
- response_json: json_dump(visible_response(body)),
198
- response_thinking_json: json_dump(thinking_response(body)),
199
- dispatch_path: body[:dispatch_path] || body[:tier],
200
- responded_at: recorded_at(body),
201
- inserted_at: Time.now.utc
202
- }, operation: 'official_record_writer.inference_response')
193
+ id = insert_with_savepoint(db, :llm_message_inference_responses, {
194
+ uuid: response_uuid,
195
+ message_inference_request_id: request[:id],
196
+ response_message_id: response_message&.dig(:id),
197
+ provider: provider(body),
198
+ provider_instance: provider_instance(body),
199
+ model_key: model_id(body),
200
+ tier: tier(body),
201
+ runner_ref: body[:worker_id] || body[:runner_ref],
202
+ provider_response_ref: body[:provider_response_ref],
203
+ status: body[:error] ? 'error' : 'success',
204
+ finish_reason: finish_reason(body),
205
+ latency_ms: integer(body[:latency_ms]),
206
+ wall_clock_ms: integer(body[:wall_clock_ms]),
207
+ response_capture_mode: 'full',
208
+ response_json: json_dump(visible_response(body)),
209
+ response_thinking_json: json_dump(thinking_response(body)),
210
+ dispatch_path: body[:dispatch_path] || body[:tier],
211
+ responded_at: recorded_at(body),
212
+ inserted_at: Time.now.utc
213
+ }, operation: 'official_record_writer.inference_response')
203
214
  db[:llm_message_inference_responses][id: id]
215
+ rescue Sequel::UniqueConstraintViolation => e
216
+ log.debug("[ledger] response collision resolved uuid=#{response_uuid} error=#{e.class}")
217
+ existing = db[:llm_message_inference_responses].where(uuid: response_uuid).first
218
+ if existing
219
+ enrich_response!(db, existing, response_message, body)
220
+ return existing
221
+ end
222
+
223
+ raise
204
224
  end
205
225
 
206
226
  def enrich_response!(db, existing, response_message, body)
@@ -237,33 +257,45 @@ module Legion
237
257
  return existing if existing
238
258
 
239
259
  token_values = tokens(body)
240
- id = insert_row(db, :llm_message_inference_metrics, {
241
- uuid: metric_uuid,
242
- message_inference_request_id: request[:id],
243
- message_inference_response_id: response[:id],
244
- provider: provider(body),
245
- model_key: model_id(body),
246
- tier: tier(body),
247
- input_tokens: token_values[:input_tokens],
248
- output_tokens: token_values[:output_tokens],
249
- thinking_tokens: token_values[:thinking_tokens],
250
- total_tokens: token_values[:total_tokens],
251
- latency_ms: integer(body[:latency_ms]),
252
- wall_clock_ms: integer(body[:wall_clock_ms]),
253
- cost_usd: cost_usd(body),
254
- currency: body[:currency] || 'USD',
255
- cost_center: billing(body)[:cost_center],
256
- budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
257
- recorded_at: recorded_at(body),
258
- inserted_at: Time.now.utc
259
- }, operation: 'official_record_writer.inference_metric')
260
+ id = insert_with_savepoint(db, :llm_message_inference_metrics, {
261
+ uuid: metric_uuid,
262
+ message_inference_request_id: request[:id],
263
+ message_inference_response_id: response[:id],
264
+ provider: provider(body),
265
+ model_key: model_id(body),
266
+ tier: tier(body),
267
+ input_tokens: token_values[:input_tokens],
268
+ output_tokens: token_values[:output_tokens],
269
+ thinking_tokens: token_values[:thinking_tokens],
270
+ total_tokens: token_values[:total_tokens],
271
+ latency_ms: integer(body[:latency_ms]),
272
+ wall_clock_ms: integer(body[:wall_clock_ms]),
273
+ cost_usd: cost_usd(body),
274
+ currency: body[:currency] || 'USD',
275
+ cost_center: billing(body)[:cost_center],
276
+ budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
277
+ recorded_at: recorded_at(body),
278
+ inserted_at: Time.now.utc
279
+ }, operation: 'official_record_writer.inference_metric')
260
280
  db[:llm_message_inference_metrics][id: id]
281
+ rescue Sequel::UniqueConstraintViolation => e
282
+ log.debug("[ledger] metric collision resolved uuid=#{metric_uuid} error=#{e.class}")
283
+ existing = db[:llm_message_inference_metrics].where(uuid: metric_uuid).first
284
+ return existing if existing
285
+
286
+ raise
261
287
  end
262
288
 
263
289
  def insert_row(db, table, attributes, operation:)
264
290
  Helpers::PersistenceLogging.insert_row(db, table, attributes, operation: operation)
265
291
  end
266
292
 
293
+ def insert_with_savepoint(db, table, attributes, operation:)
294
+ db.transaction(savepoint: true) do
295
+ insert_row(db, table, attributes, operation: operation)
296
+ end
297
+ end
298
+
267
299
  def request_ref(body)
268
300
  body[:__ledger_request_ref] ||= reference(body, :request_id, :request_ref) ||
269
301
  correlation_id(body) ||
@@ -396,19 +428,22 @@ module Legion
396
428
  existing = table.where(name: provider_name).first
397
429
  return existing if existing
398
430
 
399
- id = insert_row(db, :identity_providers, {
400
- uuid: deterministic_uuid("identity_provider:#{provider_name}"),
401
- name: provider_name,
402
- provider_type: provider_name == 'local' ? 'local' : 'external',
403
- facing: 'internal',
404
- source: 'ledger',
405
- created_at: Time.now.utc,
406
- updated_at: Time.now.utc
407
- }, operation: 'official_record_writer.identity_provider')
431
+ id = insert_with_savepoint(db, :identity_providers, {
432
+ uuid: deterministic_uuid("identity_provider:#{provider_name}"),
433
+ name: provider_name,
434
+ provider_type: provider_name == 'local' ? 'local' : 'external',
435
+ facing: 'internal',
436
+ source: 'ledger',
437
+ created_at: Time.now.utc,
438
+ updated_at: Time.now.utc
439
+ }, operation: 'official_record_writer.identity_provider')
408
440
  table[id: id]
409
441
  rescue Sequel::UniqueConstraintViolation => e
410
442
  handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_provider_race')
411
- table.where(name: provider_name).first
443
+ existing = table.where(name: provider_name).first
444
+ return existing if existing
445
+
446
+ raise
412
447
  end
413
448
 
414
449
  def find_or_create_identity_principal(db, descriptor)
@@ -416,19 +451,22 @@ module Legion
416
451
  existing = table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
417
452
  return existing if existing
418
453
 
419
- id = insert_row(db, :identity_principals, {
420
- uuid: deterministic_uuid("identity_principal:#{descriptor[:kind]}:#{descriptor[:canonical_name]}"),
421
- canonical_name: descriptor[:canonical_name],
422
- kind: descriptor[:kind],
423
- display_name: descriptor[:canonical_name],
424
- last_seen_at: Time.now.utc,
425
- created_at: Time.now.utc,
426
- updated_at: Time.now.utc
427
- }, operation: 'official_record_writer.identity_principal')
454
+ id = insert_with_savepoint(db, :identity_principals, {
455
+ uuid: deterministic_uuid("identity_principal:#{descriptor[:kind]}:#{descriptor[:canonical_name]}"),
456
+ canonical_name: descriptor[:canonical_name],
457
+ kind: descriptor[:kind],
458
+ display_name: descriptor[:canonical_name],
459
+ last_seen_at: Time.now.utc,
460
+ created_at: Time.now.utc,
461
+ updated_at: Time.now.utc
462
+ }, operation: 'official_record_writer.identity_principal')
428
463
  table[id: id]
429
464
  rescue Sequel::UniqueConstraintViolation => e
430
465
  handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_principal_race')
431
- table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
466
+ existing = table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
467
+ return existing if existing
468
+
469
+ raise
432
470
  end
433
471
 
434
472
  def find_or_create_identity(db, principal, provider, descriptor)
@@ -441,22 +479,25 @@ module Legion
441
479
  return existing if existing
442
480
 
443
481
  uuid_key = "identity:#{principal[:id]}:#{provider[:id]}:#{descriptor[:provider_identity_key]}"
444
- id = insert_row(db, :identities, {
445
- uuid: deterministic_uuid(uuid_key),
446
- principal_id: principal[:id],
447
- provider_id: provider[:id],
448
- provider_identity_key: descriptor[:provider_identity_key],
449
- last_authenticated_at: Time.now.utc,
450
- account_type: 'primary',
451
- is_default: true,
452
- created_at: Time.now.utc,
453
- updated_at: Time.now.utc
454
- }, operation: 'official_record_writer.identity')
482
+ id = insert_with_savepoint(db, :identities, {
483
+ uuid: deterministic_uuid(uuid_key),
484
+ principal_id: principal[:id],
485
+ provider_id: provider[:id],
486
+ provider_identity_key: descriptor[:provider_identity_key],
487
+ last_authenticated_at: Time.now.utc,
488
+ account_type: 'primary',
489
+ is_default: true,
490
+ created_at: Time.now.utc,
491
+ updated_at: Time.now.utc
492
+ }, operation: 'official_record_writer.identity')
455
493
  table[id: id]
456
494
  rescue Sequel::UniqueConstraintViolation => e
457
495
  handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_race')
458
- table.where(principal_id: principal[:id], provider_id: provider[:id],
459
- provider_identity_key: descriptor[:provider_identity_key]).first
496
+ existing = table.where(principal_id: principal[:id], provider_id: provider[:id],
497
+ provider_identity_key: descriptor[:provider_identity_key]).first
498
+ return existing if existing
499
+
500
+ raise
460
501
  end
461
502
 
462
503
  def identity_tables_available?(db)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-ledger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity