lex-llm-ledger 0.2.5 → 0.3.0
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bedae52b0629ce6755fc321019b9c35917e8d4f8a304c7294956cbba864fa676
|
|
4
|
+
data.tar.gz: c3f171de566906d37cb02c755f0336df1ce0f09dcdf769e84961f574e3b9d77e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8623ee921c7e0b610217925aea11e9f0f5b315b89b56d2ea239aefa9b0ac9d17b298b1f95978f784f6932ab005fa906c5c32bdd78d1f3eb47dcefb73f3762f9
|
|
7
|
+
data.tar.gz: 278ed50a3f63394470ede944211cc0893e1c8c52bda2200209161040eba3a07a0d62aa47dbfbf37e913ecdaa8d474a4144f1961b60b3e0dcfdb3af936daf155e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-08
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Renamed all `portable_identity_*` table references to canonical identity table names (`identities`, `identity_principals`, `identity_providers`).
|
|
7
|
+
- Renamed internal methods: `resolve_portable_identity` → `resolve_identity`, `find_or_create_portable_identity` → `find_or_create_identity`.
|
|
8
|
+
|
|
9
|
+
## [0.2.9] - 2026-05-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Prefer current publisher identity payloads and AMQP identity headers over stale `caller.requested_by.id` values when normalizing prompt, metering, and tool audit events.
|
|
13
|
+
- Resolve canonical caller identity strings into portable identity provider, principal, and identity rows before writing official inference request foreign keys.
|
|
14
|
+
- Store `runtime_caller_type` from explicit type fields instead of identity strings.
|
|
15
|
+
|
|
16
|
+
## [0.2.8] - 2026-05-07
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Extract caller identity from audit event structure (`identity.identity`, `caller.requested_by.identity`) instead of missing top-level `caller_identity_id` / `caller_principal_id` keys.
|
|
20
|
+
- Enrich existing request rows when prompt audit arrives after metering (backfills `caller_identity_id`, `caller_principal_id`, `runtime_caller_type`, `request_json`, `context_message_count`).
|
|
21
|
+
|
|
22
|
+
## [0.2.7] - 2026-05-07
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Enrich existing inference response rows when a richer payload arrives (prompt audit backfills `response_message_id`, `response_json`, `tier`, `finish_reason` that metering left null).
|
|
26
|
+
|
|
27
|
+
## [0.2.6] - 2026-05-07
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Add `Legion::Logging::Helper` to `OfficialRecordWriter` so `log` is available in rescue blocks.
|
|
31
|
+
- Wrap message inserts in savepoints so PostgreSQL unique constraint violations don't poison the parent transaction and cause `PG::InFailedSqlTransaction` on the fallback query.
|
|
32
|
+
|
|
3
33
|
## [0.2.5] - 2026-05-06
|
|
4
34
|
|
|
5
35
|
### Fixed
|
|
@@ -17,21 +17,21 @@ module Legion
|
|
|
17
17
|
identity_hash = identity.is_a?(Hash) ? identity : {}
|
|
18
18
|
extension = hash_value(caller_hash, :extension)
|
|
19
19
|
type = first_present(
|
|
20
|
-
hash_value(caller, :type),
|
|
21
20
|
hash_value(identity_hash, :type),
|
|
22
21
|
header_value(headers, 'x-legion-caller-type'),
|
|
22
|
+
hash_value(caller, :type),
|
|
23
23
|
extension && 'extension'
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
raw_identity = first_present(
|
|
27
|
-
hash_value(caller, :id),
|
|
28
|
-
hash_value(caller, :canonical_name),
|
|
29
27
|
hash_value(identity_hash, :id),
|
|
30
28
|
hash_value(identity_hash, :canonical_name),
|
|
31
29
|
hash_value(identity_hash, :identity),
|
|
32
30
|
hash_value(identity_hash, :username),
|
|
33
31
|
header_value(headers, 'x-legion-identity'),
|
|
34
32
|
header_value(headers, 'x-legion-caller-identity'),
|
|
33
|
+
hash_value(caller, :id),
|
|
34
|
+
hash_value(caller, :canonical_name),
|
|
35
35
|
hash_value(caller, :identity),
|
|
36
36
|
hash_value(caller, :username),
|
|
37
37
|
extension && "extension:#{extension}"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require 'securerandom'
|
|
5
|
+
require 'legion/logging'
|
|
5
6
|
require_relative '../helpers/json'
|
|
6
7
|
require_relative '../helpers/persistence_logging'
|
|
7
8
|
|
|
@@ -11,6 +12,8 @@ module Legion
|
|
|
11
12
|
module Ledger
|
|
12
13
|
module Writers
|
|
13
14
|
module OfficialRecordWriter
|
|
15
|
+
extend Legion::Logging::Helper
|
|
16
|
+
|
|
14
17
|
module_function
|
|
15
18
|
|
|
16
19
|
def write_prompt(payload)
|
|
@@ -76,34 +79,46 @@ module Legion
|
|
|
76
79
|
return existing if existing
|
|
77
80
|
|
|
78
81
|
seq = body[:message_seq] ? integer(body[:message_seq]) : next_message_seq(db, conversation)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
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
|
|
97
|
+
db[:llm_messages][id: id]
|
|
98
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
99
|
+
log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
|
|
100
|
+
db[:llm_messages].where(uuid: uuid).first ||
|
|
101
|
+
db[:llm_messages].where(conversation_id: conversation[:id], seq: seq).first
|
|
102
|
+
end
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
def find_or_create_request(db, conversation, latest_message, body)
|
|
95
106
|
request_id = request_ref(body)
|
|
96
107
|
existing = db[:llm_message_inference_requests].where(request_ref: request_id).first
|
|
97
|
-
|
|
108
|
+
if existing
|
|
109
|
+
enrich_request!(db, existing, body)
|
|
110
|
+
return existing
|
|
111
|
+
end
|
|
98
112
|
|
|
99
113
|
operation = operation(body)
|
|
114
|
+
caller_refs = caller_identity_refs(db, body)
|
|
100
115
|
id = insert_row(db, :llm_message_inference_requests, {
|
|
101
116
|
uuid: stable_uuid(request_id),
|
|
102
117
|
conversation_id: conversation[:id],
|
|
103
118
|
latest_message_id: latest_message[:id],
|
|
104
|
-
caller_principal_id:
|
|
105
|
-
caller_identity_id:
|
|
106
|
-
runtime_caller_type: body
|
|
119
|
+
caller_principal_id: caller_refs[:principal_id],
|
|
120
|
+
caller_identity_id: caller_refs[:identity_id],
|
|
121
|
+
runtime_caller_type: caller_type(body),
|
|
107
122
|
request_ref: request_id,
|
|
108
123
|
correlation_ref: correlation_id(body),
|
|
109
124
|
correlation_id: correlation_id(body),
|
|
@@ -130,27 +145,39 @@ module Legion
|
|
|
130
145
|
return existing if existing
|
|
131
146
|
|
|
132
147
|
latest = db[:llm_messages][id: request[:latest_message_id]]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
seq = (latest&.dig(:seq) || 1) + 1
|
|
149
|
+
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
|
|
166
|
+
db[:llm_messages][id: id]
|
|
167
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
168
|
+
log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
|
|
169
|
+
db[:llm_messages].where(uuid: uuid).first ||
|
|
170
|
+
db[:llm_messages].where(conversation_id: conversation[:id], seq: seq).first
|
|
171
|
+
end
|
|
148
172
|
end
|
|
149
173
|
|
|
150
174
|
def find_or_create_response(db, request, response_message, body)
|
|
151
175
|
response_uuid = stable_uuid(reference(body, :provider_response_ref) || "response:#{request_ref(body)}")
|
|
152
176
|
existing = db[:llm_message_inference_responses].where(uuid: response_uuid).first
|
|
153
|
-
|
|
177
|
+
if existing
|
|
178
|
+
enrich_response!(db, existing, response_message, body)
|
|
179
|
+
return existing
|
|
180
|
+
end
|
|
154
181
|
|
|
155
182
|
id = insert_row(db, :llm_message_inference_responses, {
|
|
156
183
|
uuid: response_uuid,
|
|
@@ -176,6 +203,34 @@ module Legion
|
|
|
176
203
|
db[:llm_message_inference_responses][id: id]
|
|
177
204
|
end
|
|
178
205
|
|
|
206
|
+
def enrich_response!(db, existing, response_message, body)
|
|
207
|
+
updates = {}
|
|
208
|
+
update_if_missing(updates, existing, :response_message_id, response_message&.dig(:id))
|
|
209
|
+
update_if_missing(updates, existing, :tier, tier(body))
|
|
210
|
+
update_if_missing(updates, existing, :provider_instance, provider_instance(body))
|
|
211
|
+
update_if_missing(updates, existing, :finish_reason, finish_reason(body))
|
|
212
|
+
update_if_missing(updates, existing, :dispatch_path, body[:dispatch_path] || body[:tier])
|
|
213
|
+
|
|
214
|
+
response_json = json_dump(visible_response(body))
|
|
215
|
+
update_if_placeholder(updates, existing, :response_json, response_json)
|
|
216
|
+
|
|
217
|
+
thinking_json = json_dump(thinking_response(body))
|
|
218
|
+
update_if_placeholder(updates, existing, :response_thinking_json, thinking_json)
|
|
219
|
+
|
|
220
|
+
return if updates.empty?
|
|
221
|
+
|
|
222
|
+
db[:llm_message_inference_responses].where(id: existing[:id]).update(updates)
|
|
223
|
+
log.info("[ledger] enriched response id=#{existing[:id]} fields=#{updates.keys.join(',')}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def update_if_missing(updates, existing, key, value)
|
|
227
|
+
updates[key] = value if existing[key].nil? && present?(value)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def update_if_placeholder(updates, existing, key, value)
|
|
231
|
+
updates[key] = value if existing[key].to_s == '{}' && value != '{}'
|
|
232
|
+
end
|
|
233
|
+
|
|
179
234
|
def find_or_create_metric(db, request, response, body)
|
|
180
235
|
metric_uuid = stable_uuid(reference(body, :message_id) || "metric:#{request_ref(body)}")
|
|
181
236
|
existing = db[:llm_message_inference_metrics].where(uuid: metric_uuid).first
|
|
@@ -222,6 +277,216 @@ module Legion
|
|
|
222
277
|
db[:llm_messages].where(id: response_message[:id]).update(message_inference_response_id: response[:id])
|
|
223
278
|
end
|
|
224
279
|
|
|
280
|
+
def enrich_request!(db, existing, body)
|
|
281
|
+
updates = {}
|
|
282
|
+
caller_refs = caller_identity_refs(db, body)
|
|
283
|
+
updates[:caller_identity_id] = caller_refs[:identity_id] if existing[:caller_identity_id].nil? && caller_refs[:identity_id]
|
|
284
|
+
updates[:caller_principal_id] = caller_refs[:principal_id] if existing[:caller_principal_id].nil? && caller_refs[:principal_id]
|
|
285
|
+
updates[:runtime_caller_type] = caller_type(body) if existing[:runtime_caller_type].nil? && caller_type(body)
|
|
286
|
+
|
|
287
|
+
request_json = json_dump(request_payload(body))
|
|
288
|
+
updates[:request_json] = request_json if existing[:request_json].to_s == '{}' && request_json != '{}'
|
|
289
|
+
|
|
290
|
+
msg_count = Array(body.dig(:request, :messages) || body[:messages]).size
|
|
291
|
+
updates[:context_message_count] = msg_count if existing[:context_message_count].to_i.zero? && msg_count.positive?
|
|
292
|
+
|
|
293
|
+
return if updates.empty?
|
|
294
|
+
|
|
295
|
+
db[:llm_message_inference_requests].where(id: existing[:id]).update(updates)
|
|
296
|
+
log.info("[ledger] enriched request id=#{existing[:id]} fields=#{updates.keys.join(',')}")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def caller_identity(body)
|
|
300
|
+
caller_identity_refs(::Legion::Data.connection, body)[:identity_id]
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def caller_principal(body)
|
|
304
|
+
caller_identity_refs(::Legion::Data.connection, body)[:principal_id]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def caller_type(body)
|
|
308
|
+
raw_type = body[:caller_type] ||
|
|
309
|
+
body.dig(:identity, :type) ||
|
|
310
|
+
body.dig(:caller, :requested_by, :type) ||
|
|
311
|
+
body.dig(:caller, :source)
|
|
312
|
+
return normalize_caller_type(raw_type) if present?(raw_type)
|
|
313
|
+
|
|
314
|
+
parsed_identity_descriptor(body)[:kind]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def caller_identity_refs(db, body)
|
|
318
|
+
body[:__ledger_caller_identity_refs] ||= begin
|
|
319
|
+
explicit_identity_id = integer_or_nil(body[:caller_identity_id] || body.dig(:caller, :requested_by, :id))
|
|
320
|
+
explicit_principal_id = integer_or_nil(body[:caller_principal_id] ||
|
|
321
|
+
body.dig(:caller, :requested_by, :principal_id))
|
|
322
|
+
refs = { principal_id: explicit_principal_id, identity_id: explicit_identity_id }.compact
|
|
323
|
+
unless refs[:principal_id] && refs[:identity_id]
|
|
324
|
+
if explicit_identity_id && !explicit_principal_id && identity_tables_available?(db)
|
|
325
|
+
row = db[:identities].where(id: explicit_identity_id).first
|
|
326
|
+
refs[:principal_id] = row[:principal_id] if row
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
resolved = resolve_identity(db, body)
|
|
330
|
+
refs[:principal_id] ||= resolved[:principal_id]
|
|
331
|
+
refs[:identity_id] ||= resolved[:identity_id]
|
|
332
|
+
end
|
|
333
|
+
refs.compact
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def resolve_identity(db, body)
|
|
338
|
+
return {} unless identity_tables_available?(db)
|
|
339
|
+
|
|
340
|
+
descriptor = parsed_identity_descriptor(body)
|
|
341
|
+
return {} unless present?(descriptor[:canonical_name])
|
|
342
|
+
|
|
343
|
+
provider = find_or_create_identity_provider(db, descriptor[:provider_name])
|
|
344
|
+
principal = find_or_create_identity_principal(db, descriptor)
|
|
345
|
+
identity = find_or_create_identity(db, principal, provider, descriptor)
|
|
346
|
+
|
|
347
|
+
{ principal_id: principal[:id], identity_id: identity[:id] }
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
handle_exception(e, level: :warn, handled: true, operation: 'official_record_writer.identity_resolution')
|
|
350
|
+
{}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def parsed_identity_descriptor(body)
|
|
354
|
+
raw_identity = body[:caller_identity] ||
|
|
355
|
+
body.dig(:identity, :identity) ||
|
|
356
|
+
body.dig(:identity, :canonical_name) ||
|
|
357
|
+
body.dig(:caller, :requested_by, :identity) ||
|
|
358
|
+
body.dig(:caller, :requested_by, :canonical_name) ||
|
|
359
|
+
body.dig(:caller, :requested_by, :id)
|
|
360
|
+
return {} unless present?(raw_identity)
|
|
361
|
+
|
|
362
|
+
raw_type = body[:caller_type] ||
|
|
363
|
+
body.dig(:identity, :type) ||
|
|
364
|
+
body.dig(:caller, :requested_by, :type) ||
|
|
365
|
+
body.dig(:caller, :source)
|
|
366
|
+
provider_name = body.dig(:identity, :credential) ||
|
|
367
|
+
body.dig(:caller, :requested_by, :credential) ||
|
|
368
|
+
'local'
|
|
369
|
+
parse_identity_descriptor(raw_identity, raw_type, provider_name)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def parse_identity_descriptor(raw_identity, raw_type, provider_name)
|
|
373
|
+
text = raw_identity.to_s
|
|
374
|
+
kind = normalize_caller_type(raw_type)
|
|
375
|
+
canonical = text
|
|
376
|
+
|
|
377
|
+
if text.include?(':') && !text.include?('@')
|
|
378
|
+
prefix, remainder = text.split(':', 2)
|
|
379
|
+
prefix_kind = normalize_caller_type(prefix)
|
|
380
|
+
if prefix_kind && present?(remainder)
|
|
381
|
+
kind ||= prefix_kind
|
|
382
|
+
canonical = remainder
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
{
|
|
387
|
+
canonical_name: canonical,
|
|
388
|
+
kind: kind || 'unknown',
|
|
389
|
+
provider_identity_key: text,
|
|
390
|
+
provider_name: normalize_provider_name(provider_name)
|
|
391
|
+
}
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def find_or_create_identity_provider(db, provider_name)
|
|
395
|
+
table = db[:identity_providers]
|
|
396
|
+
existing = table.where(name: provider_name).first
|
|
397
|
+
return existing if existing
|
|
398
|
+
|
|
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')
|
|
408
|
+
table[id: id]
|
|
409
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
410
|
+
handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_provider_race')
|
|
411
|
+
table.where(name: provider_name).first
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def find_or_create_identity_principal(db, descriptor)
|
|
415
|
+
table = db[:identity_principals]
|
|
416
|
+
existing = table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
|
|
417
|
+
return existing if existing
|
|
418
|
+
|
|
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')
|
|
428
|
+
table[id: id]
|
|
429
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
430
|
+
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
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def find_or_create_identity(db, principal, provider, descriptor)
|
|
435
|
+
table = db[:identities]
|
|
436
|
+
existing = table.where(
|
|
437
|
+
principal_id: principal[:id],
|
|
438
|
+
provider_id: provider[:id],
|
|
439
|
+
provider_identity_key: descriptor[:provider_identity_key]
|
|
440
|
+
).first
|
|
441
|
+
return existing if existing
|
|
442
|
+
|
|
443
|
+
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')
|
|
455
|
+
table[id: id]
|
|
456
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
457
|
+
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
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def identity_tables_available?(db)
|
|
463
|
+
db.table_exists?(:identity_providers) &&
|
|
464
|
+
db.table_exists?(:identity_principals) &&
|
|
465
|
+
db.table_exists?(:identities)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def normalize_caller_type(value)
|
|
469
|
+
return nil unless present?(value)
|
|
470
|
+
|
|
471
|
+
normalized = value.to_s.downcase.gsub(/[^a-z0-9_:-]+/, '_').split(':', 2).first
|
|
472
|
+
return 'human' if normalized == 'user'
|
|
473
|
+
|
|
474
|
+
normalized
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def normalize_provider_name(value)
|
|
478
|
+
raw = present?(value) ? value.to_s : 'local'
|
|
479
|
+
raw.downcase.gsub(/[^a-z0-9_.:-]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def integer_or_nil(value)
|
|
483
|
+
return nil if value.nil?
|
|
484
|
+
return value if value.is_a?(Integer)
|
|
485
|
+
|
|
486
|
+
int = value.to_s.to_i
|
|
487
|
+
int.positive? ? int : nil
|
|
488
|
+
end
|
|
489
|
+
|
|
225
490
|
def correlation_id(body)
|
|
226
491
|
reference(body, :correlation_id, :correlation_ref) || body.dig(:tracing, :correlation_id)
|
|
227
492
|
end
|
|
@@ -329,6 +594,11 @@ module Legion
|
|
|
329
594
|
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
330
595
|
end
|
|
331
596
|
|
|
597
|
+
def deterministic_uuid(value)
|
|
598
|
+
hex = Digest::SHA256.hexdigest(value.to_s)[0, 32]
|
|
599
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
600
|
+
end
|
|
601
|
+
|
|
332
602
|
def next_message_seq(db, conversation)
|
|
333
603
|
db[:llm_messages].where(conversation_id: conversation[:id]).max(:seq).to_i + 1
|
|
334
604
|
end
|