lex-llm-ledger 0.4.0 → 0.5.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: b4fb5492195845786b98e664192f84716d049c4144e81e28c5627799cf169b99
4
- data.tar.gz: fc6b2b70fbfda9e98c6e7a6e942c81f65193e5c65f46856a50b4fa64ec681972
3
+ metadata.gz: d7432d720f451a54b2826adc2eb2fa6335efb0d05cf993d2b84d29c4048090d4
4
+ data.tar.gz: a77af8bd22c3e4438b5a2c1e5ba684eb042b9612aff691fcc7085a5078ee752a
5
5
  SHA512:
6
- metadata.gz: e85919d09a173cca54608528ceb4c50a0350912a52c81a5948fff7cf483f9a428e078cbf948b7f90237191335e0e1165ff103d22864df6ffc794c2b922394b9a
7
- data.tar.gz: 6de04faae342441c24754f9b186fa92e49b6ab971ad8e4935175ad7206a58da5e288fef33791e97ed785c01f2ca0198606f82b064812a085e69530cf578dfe8d
6
+ metadata.gz: 0b03a48cfd7912f193ce78263d44cc1ca996f5d8289f971f836a9e8231b2630bd61d4ece62d1489c8fdc92414174e4e6275df088585d0a2763496e7ff673cff5
7
+ data.tar.gz: 287814a7495a4f10ba0a95f4b2f2206fadbc13bd3b6143173477dccf276ae2d15658b88e273e94abed67d0734eb9d91fee085c7b088bf7a8705e09b9e4b8e03e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] - 2026-05-26
4
+
5
+ ### Changed
6
+ - Tool audit writes no longer dead-letter when the parent response row is missing. The runner retries up to 3 times (1s delay each, configurable via `tool_write` settings), then inserts with a NULL `message_inference_response_id` instead of raising `UnrecoverableMessageError`.
7
+ - Removed `ResponseNotReady` exception class — tool calls are always persisted now.
8
+ - Populate `conversation_id` FK on `llm_tool_calls` from the message payload/headers, providing conversation-level traceability even when the response FK is NULL.
9
+ - Retry configuration moved to `default_settings[:tool_write]` (`response_retry_attempts`, `response_retry_delay`) — tunable at runtime without code changes.
10
+
11
+ ### Requires
12
+ - legion-data >= 1.8.9 (migrations 116-117)
13
+
14
+ ## [0.4.3] - 2026-05-22
15
+
16
+ ### Fixed
17
+ - Persist `llm.registry.availability` publisher identity from current transport headers into `llm_registry_availability_records`, including best-effort `identity_principal_id` and `identity_id` from DB id headers.
18
+ - Preserve legacy identity header and body fallbacks for registry availability records when current transport identity headers are absent.
19
+
20
+ ## [0.4.2] - 2026-05-22
21
+
22
+ ### Fixed
23
+ - Dead-letter tool audit messages with missing parent response rows via `UnrecoverableMessageError` so the subscription rejects the RabbitMQ delivery with `requeue: false` instead of acknowledging, republishing, or blocking inside a runner-local sleep/retry loop.
24
+ - Set the `llm.registry.availability` subscription actor prefetch to 4 so registry availability events can drain with modest concurrency.
25
+
26
+ ## [0.4.1] - 2026-05-18
27
+
28
+ ### Fixed
29
+ - Tool write retries once after 1s when parent response row is not yet committed (race between async metering publish and tool audit AMQP delivery)
30
+ - Raises `ResponseNotReady` instead of silently returning nil when response row is missing
31
+
32
+
3
33
  ## [0.4.0] - 2026-05-17
4
34
 
5
35
  ### Changed
@@ -11,6 +11,8 @@ module Legion
11
11
  class RegistryAvailability < Legion::Extensions::Actors::Subscription
12
12
  include Helpers::SubscriptionActor
13
13
 
14
+ prefetch 4
15
+
14
16
  def runner_class = Legion::Extensions::Llm::Ledger::Runners::RegistryAvailability
15
17
 
16
18
  def runner_function
@@ -29,11 +29,13 @@ module Legion
29
29
  )
30
30
 
31
31
  db = ::Legion::Data.connection
32
+ response = find_or_resolve_response_with_retry(db, body, ctx, props, headers)
32
33
  write_result = [:ok]
33
34
  db.transaction do
34
- response = find_or_resolve_response(db, body, ctx, props, headers)
35
35
  identity_attrs = extract_identity_attrs(body, headers, db)
36
- tool_call_row, new_tool_call = find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs)
36
+ conversation_id = resolve_conversation_id(db, body, ctx, headers)
37
+ tool_call_row, new_tool_call = find_or_create_tool_call(db, response, body, ctx, tool, headers,
38
+ identity_attrs, conversation_id)
37
39
  if tool_call_row && !new_tool_call
38
40
  write_result[0] = :duplicate
39
41
  elsif new_tool_call
@@ -62,8 +64,26 @@ module Legion
62
64
  Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
63
65
  end
64
66
 
65
- # Resolve the llm_message_inference_responses row this tool call belongs to.
66
- # Returns nil if we cannot link at all.
67
+ def find_or_resolve_response_with_retry(db, body, ctx, props, headers)
68
+ response = find_or_resolve_response(db, body, ctx, props, headers)
69
+ return response if response # rubocop:disable Legion/Extension/RunnerReturnHash
70
+
71
+ retry_attempts = tool_write_setting(:response_retry_attempts, 3)
72
+ retry_delay = tool_write_setting(:response_retry_delay, 1)
73
+
74
+ retry_attempts.times do |attempt|
75
+ sleep retry_delay
76
+ response = find_or_resolve_response(db, body, ctx, props, headers)
77
+ if response
78
+ log.debug("[ledger] write_tool_record: response found on retry #{attempt + 1}")
79
+ return response # rubocop:disable Legion/Extension/RunnerReturnHash
80
+ end
81
+ end
82
+
83
+ log.info('[ledger] write_tool_record: response not available after retries, proceeding with null response_id')
84
+ nil
85
+ end
86
+
67
87
  def find_or_resolve_response(db, body, ctx, props, headers)
68
88
  request_ref = ctx[:request_id] || body[:request_id] ||
69
89
  props[:correlation_id] || headers['x-legion-llm-request-id']
@@ -76,17 +96,30 @@ module Legion
76
96
  .where(message_inference_request_id: request[:id]).first
77
97
  end
78
98
 
79
- def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs)
99
+ def resolve_conversation_id(db, body, ctx, headers)
100
+ conv_ref = ctx[:conversation_id] || body[:conversation_id] ||
101
+ headers['x-legion-llm-conversation-id']
102
+ return nil unless conv_ref # rubocop:disable Legion/Extension/RunnerReturnHash
103
+
104
+ conv = db[:llm_conversations].where(uuid: stable_uuid(conv_ref)).first ||
105
+ db[:llm_conversations].where(uuid: conv_ref).first
106
+ conv&.[](:id)
107
+ end
108
+
109
+ def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs, conversation_id)
80
110
  tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
81
111
  existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
82
112
  return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
83
113
 
84
- response_id = resolve_response_id(db, response, body, ctx, headers, tool_uuid)
85
- return [nil, false] unless response_id # rubocop:disable Legion/Extension/RunnerReturnHash
114
+ response_id = response&.[](:id)
86
115
 
87
- next_index = db[:llm_tool_calls]
88
- .where(message_inference_response_id: response_id)
89
- .max(:tool_call_index).to_i + 1
116
+ next_index = if response_id
117
+ db[:llm_tool_calls]
118
+ .where(message_inference_response_id: response_id)
119
+ .max(:tool_call_index).to_i + 1
120
+ else
121
+ 0
122
+ end
90
123
 
91
124
  src = tool[:source] || {}
92
125
  status = tool[:status] || headers['x-legion-tool-status'] || 'success'
@@ -95,6 +128,7 @@ module Legion
95
128
  id = insert_with_savepoint(db, :llm_tool_calls, {
96
129
  uuid: tool_uuid,
97
130
  message_inference_response_id: response_id,
131
+ conversation_id: conversation_id,
98
132
  tool_call_index: next_index,
99
133
  provider_tool_call_ref: tool[:id],
100
134
  tool_name: tool[:name] || headers['x-legion-tool-name'],
@@ -115,35 +149,6 @@ module Legion
115
149
  [row, false]
116
150
  end
117
151
 
118
- # Extract or fall back to find a response_id for linking the tool call.
119
- def resolve_response_id(db, response, body, ctx, headers, tool_uuid)
120
- return response[:id] if response # rubocop:disable Legion/Extension/RunnerReturnHash
121
-
122
- fallback = fallback_response_for_conversation(db, body, ctx, headers)
123
- return fallback[:id] if fallback # rubocop:disable Legion/Extension/RunnerReturnHash
124
-
125
- log.warn("[ledger] write_tool_record: no response row found for tool call uuid=#{tool_uuid}, skipping")
126
- nil
127
- end
128
-
129
- def fallback_response_for_conversation(db, body, ctx, headers)
130
- conv_id = ctx[:conversation_id] || body[:conversation_id] ||
131
- headers['x-legion-llm-conversation-id']
132
- return nil unless conv_id # rubocop:disable Legion/Extension/RunnerReturnHash
133
-
134
- conv = db[:llm_conversations].where(uuid: stable_uuid(conv_id)).first ||
135
- db[:llm_conversations].where(uuid: conv_id).first
136
- return nil unless conv # rubocop:disable Legion/Extension/RunnerReturnHash
137
-
138
- db[:llm_message_inference_responses]
139
- .join(:llm_message_inference_requests,
140
- id: :message_inference_request_id)
141
- .where(Sequel[:llm_message_inference_requests][:conversation_id] => conv[:id])
142
- .order(Sequel.desc(Sequel[:llm_message_inference_responses][:id]))
143
- .select_all(:llm_message_inference_responses)
144
- .first
145
- end
146
-
147
152
  def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs) # rubocop:disable Metrics/CyclomaticComplexity
148
153
  return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
149
154
 
@@ -253,6 +258,12 @@ module Legion
253
258
  "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
254
259
  end
255
260
 
261
+ def tool_write_setting(key, default)
262
+ ledger = Legion::Settings.dig(:extensions, :llm, :ledger) || {}
263
+ tool_write = ledger[:tool_write] || {}
264
+ (tool_write[key] || default).to_i
265
+ end
266
+
256
267
  def insert_with_savepoint(db, table, attributes, operation:)
257
268
  db.transaction(savepoint: true) do
258
269
  Helpers::PersistenceLogging.insert_row(db, table, attributes,
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Ledger
7
- VERSION = '0.4.0'
7
+ VERSION = '0.5.0'
8
8
  end
9
9
  end
10
10
  end
@@ -51,9 +51,13 @@ module Legion
51
51
 
52
52
  def self.default_settings
53
53
  {
54
- retention: {
54
+ retention: {
55
55
  default_days: 90,
56
56
  phi_ttl_days: 30
57
+ },
58
+ tool_write: {
59
+ response_retry_attempts: 3,
60
+ response_retry_delay: 1
57
61
  }
58
62
  }
59
63
  end
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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity