lex-llm-ledger 0.4.2 → 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: 9d96639d6f6bf123c338c0df58c4e25d1375d0102873318197f6297eeda70a80
4
- data.tar.gz: 1ebcbc88558e3783595e3c07c13eb0f9d0f3fbb82a2c096cb52752592c469d2a
3
+ metadata.gz: d7432d720f451a54b2826adc2eb2fa6335efb0d05cf993d2b84d29c4048090d4
4
+ data.tar.gz: a77af8bd22c3e4438b5a2c1e5ba684eb042b9612aff691fcc7085a5078ee752a
5
5
  SHA512:
6
- metadata.gz: 670f7c45555f557aae91ba858831f1ce40e74eaf92a5390a965e2d5e6487a6b6deb9700527a6098bd8454b2085fbe1de567195d4a7f54b8f910921d6dde9535f
7
- data.tar.gz: a72b7dac0f47bf49ca637f060698899d8eb2fc1d7436e2271afbddb8ef5684a00e5b75ae264ac6d635bc9ac58b9e79fab84070b4b51878324615d939942a9ef7
6
+ metadata.gz: 0b03a48cfd7912f193ce78263d44cc1ca996f5d8289f971f836a9e8231b2630bd61d4ece62d1489c8fdc92414174e4e6275df088585d0a2763496e7ff673cff5
7
+ data.tar.gz: 287814a7495a4f10ba0a95f4b2f2206fadbc13bd3b6143173477dccf276ae2d15658b88e273e94abed67d0734eb9d91fee085c7b088bf7a8705e09b9e4b8e03e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## [0.4.2] - 2026-05-22
4
21
 
5
22
  ### Fixed
@@ -10,8 +10,6 @@ module Legion
10
10
  module Extensions
11
11
  module Llm
12
12
  module Ledger
13
- class ResponseNotReady < StandardError; end
14
-
15
13
  module Runners
16
14
  module Tools
17
15
  extend self
@@ -31,11 +29,13 @@ module Legion
31
29
  )
32
30
 
33
31
  db = ::Legion::Data.connection
32
+ response = find_or_resolve_response_with_retry(db, body, ctx, props, headers)
34
33
  write_result = [:ok]
35
34
  db.transaction do
36
- response = find_or_resolve_response(db, body, ctx, props, headers)
37
35
  identity_attrs = extract_identity_attrs(body, headers, db)
38
- 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)
39
39
  if tool_call_row && !new_tool_call
40
40
  write_result[0] = :duplicate
41
41
  elsif new_tool_call
@@ -53,9 +53,6 @@ module Legion
53
53
  rescue Helpers::DecryptionFailed => e
54
54
  handle_exception(e, level: :error, handled: true, operation: 'write_tool_record.decrypt')
55
55
  raise
56
- rescue ResponseNotReady => e
57
- log.warn("[ledger] write_tool_record: parent response not yet written, dead-lettering message (#{e.message})")
58
- raise unrecoverable_message_error(e)
59
56
  rescue StandardError => e
60
57
  handle_exception(e, level: :error, handled: true, operation: 'write_tool_record')
61
58
  { result: :error, error: e.message }
@@ -67,16 +64,26 @@ module Legion
67
64
  Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
68
65
  end
69
66
 
70
- def unrecoverable_message_error(error)
71
- if defined?(Legion::Extensions::Actors::UnrecoverableMessageError)
72
- Legion::Extensions::Actors::UnrecoverableMessageError.new(error.message)
73
- else
74
- error
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
75
81
  end
82
+
83
+ log.info('[ledger] write_tool_record: response not available after retries, proceeding with null response_id')
84
+ nil
76
85
  end
77
86
 
78
- # Resolve the llm_message_inference_responses row this tool call belongs to.
79
- # Returns nil if we cannot link at all.
80
87
  def find_or_resolve_response(db, body, ctx, props, headers)
81
88
  request_ref = ctx[:request_id] || body[:request_id] ||
82
89
  props[:correlation_id] || headers['x-legion-llm-request-id']
@@ -89,17 +96,30 @@ module Legion
89
96
  .where(message_inference_request_id: request[:id]).first
90
97
  end
91
98
 
92
- 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)
93
110
  tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
94
111
  existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
95
112
  return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
96
113
 
97
- response_id = resolve_response_id(db, response, body, ctx, headers, tool_uuid)
98
- return [nil, false] unless response_id # rubocop:disable Legion/Extension/RunnerReturnHash
114
+ response_id = response&.[](:id)
99
115
 
100
- next_index = db[:llm_tool_calls]
101
- .where(message_inference_response_id: response_id)
102
- .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
103
123
 
104
124
  src = tool[:source] || {}
105
125
  status = tool[:status] || headers['x-legion-tool-status'] || 'success'
@@ -108,6 +128,7 @@ module Legion
108
128
  id = insert_with_savepoint(db, :llm_tool_calls, {
109
129
  uuid: tool_uuid,
110
130
  message_inference_response_id: response_id,
131
+ conversation_id: conversation_id,
111
132
  tool_call_index: next_index,
112
133
  provider_tool_call_ref: tool[:id],
113
134
  tool_name: tool[:name] || headers['x-legion-tool-name'],
@@ -128,34 +149,6 @@ module Legion
128
149
  [row, false]
129
150
  end
130
151
 
131
- # Extract or fall back to find a response_id for linking the tool call.
132
- def resolve_response_id(db, response, body, ctx, headers, tool_uuid)
133
- return response[:id] if response # rubocop:disable Legion/Extension/RunnerReturnHash
134
-
135
- fallback = fallback_response_for_conversation(db, body, ctx, headers)
136
- return fallback[:id] if fallback # rubocop:disable Legion/Extension/RunnerReturnHash
137
-
138
- raise ResponseNotReady, "no response row found for tool call uuid=#{tool_uuid}"
139
- end
140
-
141
- def fallback_response_for_conversation(db, body, ctx, headers)
142
- conv_id = ctx[:conversation_id] || body[:conversation_id] ||
143
- headers['x-legion-llm-conversation-id']
144
- return nil unless conv_id # rubocop:disable Legion/Extension/RunnerReturnHash
145
-
146
- conv = db[:llm_conversations].where(uuid: stable_uuid(conv_id)).first ||
147
- db[:llm_conversations].where(uuid: conv_id).first
148
- return nil unless conv # rubocop:disable Legion/Extension/RunnerReturnHash
149
-
150
- db[:llm_message_inference_responses]
151
- .join(:llm_message_inference_requests,
152
- id: :message_inference_request_id)
153
- .where(Sequel[:llm_message_inference_requests][:conversation_id] => conv[:id])
154
- .order(Sequel.desc(Sequel[:llm_message_inference_responses][:id]))
155
- .select_all(:llm_message_inference_responses)
156
- .first
157
- end
158
-
159
152
  def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs) # rubocop:disable Metrics/CyclomaticComplexity
160
153
  return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
161
154
 
@@ -265,6 +258,12 @@ module Legion
265
258
  "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
266
259
  end
267
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
+
268
267
  def insert_with_savepoint(db, table, attributes, operation:)
269
268
  db.transaction(savepoint: true) do
270
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.2'
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.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity