lex-microsoft_teams 0.6.51 → 0.6.53

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/CHANGELOG.md +10 -1
  4. data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +3 -3
  5. data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +3 -3
  6. data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +2 -2
  7. data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +2 -2
  8. data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +2 -2
  9. data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +2 -2
  10. data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +16 -6
  11. data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +1 -1
  12. data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +1 -1
  13. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +1 -1
  14. data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +1 -1
  15. data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +14 -6
  16. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
  17. data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +14 -5
  18. data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +2 -2
  19. data/lib/legion/extensions/microsoft_teams/faraday/retry_after.rb +1 -1
  20. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +4 -4
  21. data/lib/legion/extensions/microsoft_teams/helpers/graph_cache.rb +3 -3
  22. data/lib/legion/extensions/microsoft_teams/helpers/graph_client.rb +1 -1
  23. data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +1 -1
  24. data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +1 -1
  25. data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +1 -1
  26. data/lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb +185 -0
  27. data/lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb +1 -1
  28. data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +1 -1
  29. data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +91 -19
  30. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +2 -2
  31. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +2 -2
  32. data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +89 -35
  33. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  34. data/lib/legion/extensions/microsoft_teams.rb +3 -1
  35. metadata +2 -1
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'monitor'
4
+ require 'legion/extensions/microsoft_teams/errors'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module MicrosoftTeams
9
+ module Helpers
10
+ # Mixin that lets an `Actors::Every`-style poller honour a Graph
11
+ # throttle by *deferring its own next run* instead of re-firing on
12
+ # its fixed timer interval.
13
+ #
14
+ # Background: `Faraday::RetryAfter` raises `Errors::Throttled`
15
+ # centrally when it exhausts retries (or when the advertised wait
16
+ # alone exceeds the retry budget), and `Faraday::ThrottleCircuit`
17
+ # opens a shared circuit so the whole fleet stops hammering a quota
18
+ # that Graph has already flagged. But the actors that *drive* those
19
+ # calls schedule themselves with a `Concurrent::TimerTask` whose
20
+ # `execution_interval` is fixed at boot. Catching `Throttled` in a
21
+ # `rescue` block stops the current tick, but the next tick still
22
+ # fires on the original cadence — so a poller on a 30–300s interval
23
+ # keeps walking straight back into an open circuit, emitting an
24
+ # ERROR every interval and contributing nothing but log noise (and,
25
+ # via the shared circuit, refreshing the block for cheaper callers).
26
+ #
27
+ # This mixin closes that gap. An actor wraps its Graph work in
28
+ # {#with_throttle_deferral}. On `Errors::Throttled` it records a
29
+ # "suppress until" instant of `now + retry_after` (falling back to a
30
+ # bounded default when the server gave no usable `Retry-After`).
31
+ # Subsequent ticks short-circuit via {#throttle_suppressed?} until
32
+ # that instant passes, so the actor effectively reschedules itself
33
+ # at `now + retry_after` rather than `now + interval` — exactly the
34
+ # behaviour the throttle integration spec and the 0.6.51 CHANGELOG
35
+ # flagged as the outstanding follow-up.
36
+ #
37
+ # State is per-actor-instance and guarded by a monitor because an
38
+ # actor's `manual` runs on a `Concurrent` thread-pool worker. The
39
+ # mixin reads the carried `retry_after` only when
40
+ # `retry_after_known?` is true; otherwise it applies
41
+ # {DEFAULT_DEFERRAL} so a throttle with a missing/garbage header
42
+ # still backs the poller off rather than letting it spin.
43
+ module ThrottleAware
44
+ # Fallback deferral (seconds) used when a `Throttled` carries no
45
+ # usable `retry_after` (header absent or unparseable). One minute
46
+ # matches Microsoft Graph's documented typical Retry-After and the
47
+ # circuit middleware's `DEFAULT_FALLBACK_TTL`.
48
+ DEFAULT_DEFERRAL = 60.0
49
+
50
+ # Hard ceiling (seconds) on a single deferral so a pathological
51
+ # advertised `Retry-After` can't park a poller for an unbounded
52
+ # time. Mirrors the circuit middleware's 600s cap in
53
+ # `ThrottleCircuit#set_hard_circuit`.
54
+ MAX_DEFERRAL = 600.0
55
+
56
+ # Run `block` unless the actor is currently deferring after a
57
+ # recent throttle. If a `Throttled` escapes the block, record the
58
+ # deferral window and swallow it (the throttle has already been
59
+ # logged at the middleware/circuit layer; re-raising would just
60
+ # trip the actor's generic `rescue StandardError` and double-log).
61
+ #
62
+ # @param now [Time] injectable clock for deterministic tests
63
+ # @return [Object, nil] the block's value, or nil when suppressed
64
+ # or when a throttle was caught
65
+ def with_throttle_deferral(now: Time.now)
66
+ if throttle_suppressed?(now: now)
67
+ log_throttle_skip(now: now)
68
+ return nil
69
+ end
70
+
71
+ yield
72
+ rescue Legion::Extensions::MicrosoftTeams::Errors::Throttled => e
73
+ defer_after_throttle(e, now: now)
74
+ nil
75
+ end
76
+
77
+ # @return [Boolean] true while the actor is inside a deferral
78
+ # window opened by a previous throttle.
79
+ def throttle_suppressed?(now: Time.now)
80
+ until_at = throttled_until
81
+ !until_at.nil? && now < until_at
82
+ end
83
+
84
+ # The instant before which the actor should not poll again, or nil
85
+ # if no deferral is active. Thread-safe read.
86
+ def throttled_until
87
+ throttle_monitor.synchronize { @throttled_until }
88
+ end
89
+
90
+ # Seconds remaining in the current deferral window (0.0 if none /
91
+ # elapsed). Useful for logging and for tests.
92
+ def throttle_remaining(now: Time.now)
93
+ until_at = throttled_until
94
+ return 0.0 if until_at.nil?
95
+
96
+ remaining = until_at - now
97
+ remaining.positive? ? remaining : 0.0
98
+ end
99
+
100
+ # Open (or extend) a deferral window of `seconds` from `now`. A
101
+ # later throttle never *shortens* an existing window — we keep the
102
+ # furthest-out instant so overlapping throttles compose safely.
103
+ def defer_for(seconds, now: Time.now)
104
+ window = clamp_deferral(seconds)
105
+ target = now + window
106
+ throttle_monitor.synchronize do
107
+ @throttled_until = if @throttled_until.nil? || target > @throttled_until
108
+ target
109
+ else
110
+ @throttled_until
111
+ end
112
+ end
113
+ window
114
+ end
115
+
116
+ # Clear any active deferral. Called implicitly is unnecessary —
117
+ # {#throttle_suppressed?} expires on its own once the clock passes
118
+ # `throttled_until` — but exposed for tests and explicit resets.
119
+ def reset_throttle_deferral
120
+ throttle_monitor.synchronize { @throttled_until = nil }
121
+ end
122
+
123
+ private
124
+
125
+ def defer_after_throttle(error, now: Time.now)
126
+ seconds = deferral_seconds_for(error)
127
+ window = defer_for(seconds, now: now)
128
+ log.warn(
129
+ "[microsoft_teams][throttle_defer] #{actor_label}: " \
130
+ "deferring next run #{format('%.1f', window)}s " \
131
+ "(retry_after=#{error.retry_after_known? ? format('%.1f', error.retry_after) : 'unknown'} " \
132
+ "path=#{error.request})"
133
+ )
134
+ end
135
+
136
+ # Short, log-friendly name for the including actor. Falls back to
137
+ # the full class string for anonymous classes (whose `name` is
138
+ # nil), so the log line never raises on `name.split`.
139
+ def actor_label
140
+ (self.class.name || self.class.to_s).split('::').last
141
+ end
142
+
143
+ # Prefer the server-advised wait; fall back to {DEFAULT_DEFERRAL}
144
+ # only when the header was absent or unparseable.
145
+ def deferral_seconds_for(error)
146
+ if error.respond_to?(:retry_after_known?) && error.retry_after_known?
147
+ error.retry_after
148
+ else
149
+ DEFAULT_DEFERRAL
150
+ end
151
+ end
152
+
153
+ def clamp_deferral(seconds)
154
+ value = begin
155
+ Float(seconds)
156
+ # rubocop:disable Legion/RescueLogging/NoCapture
157
+ # Pure coercion fallback for a non-numeric deferral; the value
158
+ # is bounded immediately below, so a bad input degrades to the
159
+ # default rather than warranting its own log line.
160
+ rescue ArgumentError, TypeError
161
+ DEFAULT_DEFERRAL
162
+ # rubocop:enable Legion/RescueLogging/NoCapture
163
+ end
164
+ value = DEFAULT_DEFERRAL if value <= 0
165
+ [value, MAX_DEFERRAL].min
166
+ end
167
+
168
+ def log_throttle_skip(now: Time.now)
169
+ log.debug(
170
+ "[microsoft_teams][throttle_defer] #{actor_label}: " \
171
+ "skipping run, #{format('%.1f', throttle_remaining(now: now))}s deferral remaining"
172
+ )
173
+ end
174
+
175
+ def throttle_monitor
176
+ # `||=` is safe here: the first tick of an actor runs before any
177
+ # concurrent re-entry (the `Every` base guards re-entry with its
178
+ # own AtomicBoolean), so the monitor is initialised single-threaded.
179
+ @throttle_monitor ||= Monitor.new
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -131,7 +131,7 @@ module Legion
131
131
  return unless defined?(log)
132
132
 
133
133
  if defined?(handle_exception)
134
- handle_exception(error, level: :debug, operation: "TraceRetriever##{method}")
134
+ handle_exception(error, level: :warn, operation: "TraceRetriever##{method}")
135
135
  else
136
136
  log.debug("TraceRetriever##{method} failed: #{error.message}")
137
137
  end
@@ -102,7 +102,7 @@ module Legion
102
102
  def decode_varint(data, pos)
103
103
  result = 0
104
104
  shift = 0
105
- loop do
105
+ 10.times do # varint max 10 bytes (64-bit)
106
106
  return [nil, pos] if pos >= data.bytesize
107
107
 
108
108
  byte = data.getbyte(pos)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'digest'
5
+ require 'legion/extensions/microsoft_teams/errors'
5
6
  require 'legion/extensions/microsoft_teams/helpers/client'
6
7
  require 'legion/extensions/microsoft_teams/helpers/graph_cache'
7
8
  require 'legion/extensions/microsoft_teams/helpers/permission_guard'
@@ -21,16 +22,12 @@ module Legion
21
22
 
22
23
  definition :ingest_api, mcp_exposed: false
23
24
 
24
- # Fetch top contacts via /me/people, then pull recent messages from
25
- # their 1:1 chats. Stores each message as an individual memory trace
26
- # (same format as CacheIngest) with dedup by content hash.
27
- #
28
- # Requires a delegated token with Chat.Read and People.Read scopes.
29
25
  def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
30
26
  log.debug("ApiIngest#ingest_api top_people=#{top_people} message_depth=#{message_depth}")
31
27
  return error_result('lex-memory not loaded') unless memory_available?
32
28
  return error_result('no token provided') unless token && !token.empty?
33
29
 
30
+ @fetch_failures = 0
34
31
  restore_hwm_from_traces
35
32
 
36
33
  people = fetch_top_people(token: token, top: top_people)
@@ -103,16 +100,29 @@ module Legion
103
100
 
104
101
  { result: { stored: stored, skipped: skipped, people_ingested: people_ingested,
105
102
  people_found: people.length, chats_found: chats.length,
103
+ fetch_failures: @fetch_failures,
106
104
  apollo: apollo_results } }
105
+ # rubocop:disable Legion/RescueLogging/NoCapture
106
+ # Re-raise unlogged: surface the typed throttle to the caller (the
107
+ # ApiIngest actor) so it can defer its next scheduled run by the
108
+ # advertised retry_after. The throttle is already logged at the
109
+ # middleware/circuit layer; folding it into an error result here
110
+ # would hide the one signal the actor needs to stop re-charging
111
+ # the shared Graph circuit on its fixed interval.
112
+ rescue Errors::Throttled
113
+ raise
114
+ # rubocop:enable Legion/RescueLogging/NoCapture
107
115
  rescue StandardError => e
108
116
  handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
109
- { result: { stored: stored || 0, skipped: skipped || 0, error: e.message } }
117
+ { result: { stored: stored || 0, skipped: skipped || 0,
118
+ fetch_failures: @fetch_failures || 0, error: e.message } }
110
119
  end
111
120
 
112
121
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
113
122
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
114
123
 
115
124
  MAX_CHAT_PAGES = 10
125
+ CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
116
126
 
117
127
  private
118
128
 
@@ -124,15 +134,24 @@ module Legion
124
134
  resp = conn.get('me/people', { '$top' => top })
125
135
 
126
136
  log.debug("ApiIngest: fetch_top_people status=#{resp.status} count=#{(resp.body || {}).fetch('value', []).size}")
127
- if resp.status == 403
128
- record_denial('/me/people', resp.body.dig('error', 'message') || 'Forbidden')
137
+ unless (200..299).cover?(resp.status)
138
+ error_code = resp.body&.dig('error', 'code')
139
+ log.warn('[microsoft_teams][api_ingest] fetch_top_people non-2xx: ' \
140
+ "status=#{resp.status} error_code=#{error_code}")
141
+ record_denial('/me/people', resp.body&.dig('error', 'message') || 'Forbidden') if resp.status == 403
142
+ @fetch_failures = (@fetch_failures || 0) + 1
129
143
  return []
130
144
  end
131
145
 
132
146
  people = (resp.body || {}).fetch('value', [])
133
147
  people.sort_by { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
148
+ # rubocop:disable Legion/RescueLogging/NoCapture
149
+ rescue Errors::Throttled
150
+ raise
151
+ # rubocop:enable Legion/RescueLogging/NoCapture
134
152
  rescue StandardError => e
135
153
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_top_people')
154
+ @fetch_failures = (@fetch_failures || 0) + 1
136
155
  []
137
156
  end
138
157
 
@@ -143,8 +162,17 @@ module Legion
143
162
  params = { '$top' => 50 }
144
163
  pages = 0
145
164
 
146
- loop do
165
+ MAX_CHAT_PAGES.times do
147
166
  resp = conn.get(url, params)
167
+
168
+ unless (200..299).cover?(resp.status)
169
+ error_code = resp.body&.dig('error', 'code')
170
+ log.warn('[microsoft_teams][api_ingest] fetch_one_on_one_chats non-2xx: ' \
171
+ "status=#{resp.status} error_code=#{error_code}")
172
+ @fetch_failures = (@fetch_failures || 0) + 1
173
+ break
174
+ end
175
+
148
176
  body = resp.body || {}
149
177
  chats = body.fetch('value', [])
150
178
  all_chats.concat(chats)
@@ -152,7 +180,6 @@ module Legion
152
180
 
153
181
  next_link = body['@odata.nextLink']
154
182
  break unless next_link
155
- break if pages >= MAX_CHAT_PAGES
156
183
 
157
184
  url = next_link
158
185
  params = {}
@@ -162,8 +189,13 @@ module Legion
162
189
  filtered = all_chats.select { |c| allowed_types.include?(c['chatType']) }
163
190
  log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{filtered.size} eligible (1:1/group/meeting)")
164
191
  filtered
192
+ # rubocop:disable Legion/RescueLogging/NoCapture
193
+ rescue Errors::Throttled
194
+ raise
195
+ # rubocop:enable Legion/RescueLogging/NoCapture
165
196
  rescue StandardError => e
166
197
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_one_on_one_chats')
198
+ @fetch_failures = (@fetch_failures || 0) + 1
167
199
  []
168
200
  end
169
201
 
@@ -172,9 +204,10 @@ module Legion
172
204
  by_user_id = {}
173
205
  by_name = {}
174
206
 
175
- chats.each do |chat|
207
+ sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
208
+ sorted.each do |chat|
176
209
  members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
177
- shared: true)
210
+ shared: true, ttl: members_cache_ttl)
178
211
  .then { |body| (body || {}).fetch('value', []) }
179
212
  members.each do |m|
180
213
  email = m['email']&.downcase
@@ -190,8 +223,13 @@ module Legion
190
223
  end
191
224
 
192
225
  { email: by_email, user_id: by_user_id, name: by_name }
226
+ # rubocop:disable Legion/RescueLogging/NoCapture
227
+ rescue Errors::Throttled
228
+ raise
229
+ # rubocop:enable Legion/RescueLogging/NoCapture
193
230
  rescue StandardError => e
194
231
  handle_exception(e, level: :warn, operation: 'ApiIngest#build_chat_member_index')
232
+ @fetch_failures = (@fetch_failures || 0) + 1
195
233
  { email: {}, user_id: {}, name: {} }
196
234
  end
197
235
 
@@ -205,17 +243,41 @@ module Legion
205
243
  chat_index[:name][display_name]
206
244
  end
207
245
 
246
+ def hwm_client_side_cut(messages:, hwm:)
247
+ return messages unless hwm&.dig(:last_message_at)
248
+
249
+ cutoff = hwm[:last_message_at]
250
+ seen = Set.new
251
+ messages.take_while { |m| m['createdDateTime'] && m['createdDateTime'] > cutoff }
252
+ .select { |m| seen.add?(m['id'] || Digest::SHA256.hexdigest(m.dig('body', 'content').to_s)[0, 16]) }
253
+ end
254
+
208
255
  def fetch_chat_messages(conn:, chat_id:, depth: 50)
209
256
  hwm = get_extended_hwm(chat_id: chat_id)
210
257
  params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
211
- params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
212
258
 
213
259
  resp = conn.get("chats/#{chat_id}/messages", params)
214
- log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{(resp.body || {}).fetch('value', []).size}")
215
- (resp.body || {}).fetch('value', [])
260
+
261
+ unless (200..299).cover?(resp.status)
262
+ error_code = resp.body&.dig('error', 'code')
263
+ log.warn('[microsoft_teams][api_ingest] fetch_chat_messages non-2xx: ' \
264
+ "chat_id=#{chat_id} status=#{resp.status} error_code=#{error_code}")
265
+ @fetch_failures = (@fetch_failures || 0) + 1
266
+ return []
267
+ end
268
+
269
+ messages = (resp.body || {}).fetch('value', [])
270
+ messages = hwm_client_side_cut(messages: messages, hwm: hwm)
271
+ log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{messages.size}")
272
+ messages
273
+ # rubocop:disable Legion/RescueLogging/NoCapture
274
+ rescue Errors::Throttled
275
+ raise
276
+ # rubocop:enable Legion/RescueLogging/NoCapture
216
277
  rescue StandardError => e
217
278
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_chat_messages',
218
279
  chat_id: chat_id)
280
+ @fetch_failures = (@fetch_failures || 0) + 1
219
281
  []
220
282
  end
221
283
 
@@ -284,7 +346,7 @@ module Legion
284
346
  end
285
347
  hashes
286
348
  rescue StandardError => e
287
- handle_exception(e, level: :debug, operation: 'ApiIngest#load_existing_hashes')
349
+ handle_exception(e, level: :warn, operation: 'ApiIngest#load_existing_hashes')
288
350
  Set.new
289
351
  end
290
352
 
@@ -313,12 +375,12 @@ module Legion
313
375
  trace_ids.each_cons(2) do |id_a, id_b|
314
376
  store.record_coactivation(id_a, id_b)
315
377
  rescue StandardError => e
316
- handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces',
378
+ handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces',
317
379
  id_a: id_a, id_b: id_b)
318
380
  end
319
381
  end
320
382
  rescue StandardError => e
321
- handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces')
383
+ handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces')
322
384
  end
323
385
 
324
386
  def publish_to_apollo(person_texts)
@@ -377,7 +439,7 @@ module Legion
377
439
 
378
440
  { success: true, count: result[:entities].length }
379
441
  rescue StandardError => e
380
- handle_exception(e, level: :debug, operation: 'ApiIngest#extract_and_ingest_entities',
442
+ handle_exception(e, level: :warn, operation: 'ApiIngest#extract_and_ingest_entities',
381
443
  person_name: person_name)
382
444
  { success: false, count: 0 }
383
445
  end
@@ -396,6 +458,16 @@ module Legion
396
458
  @apollo_knowledge_runner ||= Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
397
459
  end
398
460
 
461
+ def members_cache_ttl
462
+ return @members_cache_ttl if defined?(@members_cache_ttl)
463
+
464
+ @members_cache_ttl = if respond_to?(:settings, true) && settings.respond_to?(:dig)
465
+ settings.dig(:cache, :members_ttl) || 86_400
466
+ else
467
+ 86_400
468
+ end
469
+ end
470
+
399
471
  def error_result(message)
400
472
  { result: { stored: 0, skipped: 0, error: message } }
401
473
  end
@@ -200,7 +200,7 @@ module Legion
200
200
 
201
201
  retrieve_context(message: message, owner_id: owner_id, chat_id: chat_id)
202
202
  rescue StandardError => e
203
- handle_exception(e, level: :debug, operation: 'Bot#retrieve_trace_context') if defined?(handle_exception)
203
+ handle_exception(e, level: :warn, operation: 'Bot#retrieve_trace_context') if defined?(handle_exception)
204
204
  nil
205
205
  end
206
206
 
@@ -231,7 +231,7 @@ module Legion
231
231
  Legion::Settings.dig(:microsoft_teams, :bot, :observe, :enabled) == true
232
232
  end
233
233
 
234
- def notify_enabled?(**_kwargs)
234
+ def notify_enabled?(**)
235
235
  return false unless defined?(Legion::Settings)
236
236
 
237
237
  Legion::Settings.dig(:microsoft_teams, :bot, :observe, :notify) == true
@@ -123,12 +123,12 @@ module Legion
123
123
  trace_ids.each_cons(2) do |id_a, id_b|
124
124
  store.record_coactivation(id_a, id_b)
125
125
  rescue StandardError => e
126
- handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces',
126
+ handle_exception(e, level: :warn, operation: 'CacheIngest#coactivate_thread_traces',
127
127
  id_a: id_a, id_b: id_b)
128
128
  end
129
129
  end
130
130
  rescue StandardError => e
131
- handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces')
131
+ handle_exception(e, level: :warn, operation: 'CacheIngest#coactivate_thread_traces')
132
132
  end
133
133
  end
134
134
  end