lex-microsoft_teams 0.6.52 → 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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/CHANGELOG.md +4 -0
  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 +2 -2
  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 +1 -1
  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 +1 -1
  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/trace_retriever.rb +1 -1
  27. data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +1 -1
  28. data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +80 -19
  29. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +2 -2
  30. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +2 -2
  31. data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +39 -35
  32. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  33. data/lib/legion/extensions/microsoft_teams.rb +2 -1
  34. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac161b519f294da86eb8956d3aab55f11f4cd87d7fe710c43c033640b630efe5
4
- data.tar.gz: bf64b0446e44cd89f9f9cc8be018a2629f0d375ccbfea4848e4668a16ad04598
3
+ metadata.gz: 1c452da3646f36302d562fefd816a7b00621e95d819017c6125cc7c0fcb1ea74
4
+ data.tar.gz: c11fa2558cb00fa66d884360242f98a574b68d4ecda61451ba7e1a5de3b8067c
5
5
  SHA512:
6
- metadata.gz: 926fd8d8f6aa8bbe022fbb17491b959faea7580dd28dee76c48199392fb3d50bd917934ed4025f4a65f20f7353412abbc6788de4634ebeb00d3bf07497e53e28
7
- data.tar.gz: 26ee2c81c7e0330b575b2aedb8f2284d86c4d40a5b42d089b8edce9a6cd33b06699cf2bac2e7671ff62eff830940ed179b27db1cbcdf2e171d856916a24a296f
6
+ metadata.gz: db1531411cd4b67f36b644652e3549aae7b9f3360a987187f04a8bc39c9d436bb8dea21873f61edcbf85964b6bcf5889e68bd64bd786f1967f0426f5aede2d51
7
+ data.tar.gz: 6798cc2a718ac3ba43db14b3476abaeab0279d9747c90c8d47f6dfd185f86271787f48042ab22d0dcdfce48af5d7cdf505bb4bf0766601d0d2bfdd8f37bfeb7a
data/.rubocop.yml CHANGED
@@ -16,6 +16,10 @@ Legion/Extension/EveryActorRequiresTime:
16
16
  # RunnerReturnHash fires on private helper methods inside runner modules (false positives)
17
17
  Legion/Extension/RunnerReturnHash:
18
18
  Enabled: false
19
+
20
+ # This extension uses trace types (episodic, semantic, procedural) not LLM taxonomy types
21
+ Legion/Llm/TaxonomyEnum:
22
+ Enabled: false
19
23
 
20
24
  Metrics/CyclomaticComplexity:
21
25
  Max: 25
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.53] - 2026-07-15
4
+ ### Fixed
5
+ - Fix throttle propagation, invalid OData filter, member index priority, stop trace spam from API fetches
6
+
3
7
  ## [0.6.52] - 2026-05-29
4
8
  ### Fixed
5
9
  - **Actors now defer their next scheduled run on a Graph throttle** instead of re-firing on their fixed timer interval — resolves the 0.6.51 "Known follow-up". New `Helpers::ThrottleAware` mixin records a "suppress until" instant of `now + retry_after` when a Graph call raises `Errors::Throttled`; subsequent ticks short-circuit until that window elapses. Wired into `PresencePoller`, `MeetingIngest`, and `ApiIngest`. Falls back to a bounded 60s deferral when the server gave no usable `Retry-After`, and clamps any single deferral to 600s. Previously a poller on a 30–300s interval would walk straight back into the still-open shared circuit every interval, emitting a `[throttle_circuit] hard circuit` ERROR (and a duplicate `Errors::Throttled` WARN/ERROR) each tick while ingesting nothing.
@@ -57,7 +57,7 @@ module Legion
57
57
  def graph_token
58
58
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
59
59
  rescue StandardError => e
60
- handle_exception(e, level: :debug, operation: 'Channel#graph_token')
60
+ handle_exception(e, level: :warn, operation: 'Channel#graph_token')
61
61
  nil
62
62
  end
63
63
 
@@ -85,7 +85,7 @@ module Legion
85
85
 
86
86
  { team_id: team_id, channel_id: channel_id, message_id: message_id }
87
87
  rescue StandardError => e
88
- handle_exception(e, level: :debug, operation: 'Channel#extract_ids', url: url)
88
+ handle_exception(e, level: :warn, operation: 'Channel#extract_ids', url: url)
89
89
  nil
90
90
  end
91
91
 
@@ -202,7 +202,7 @@ module Legion
202
202
  " ↳ [#{timestamp}] #{sender}: #{text}"
203
203
  end
204
204
  rescue StandardError => e
205
- handle_exception(e, level: :debug, operation: 'Channel#fetch_reply_lines',
205
+ handle_exception(e, level: :warn, operation: 'Channel#fetch_reply_lines',
206
206
  team_id: team_id, channel_id: channel_id, message_id: message_id)
207
207
  []
208
208
  end
@@ -46,7 +46,7 @@ module Legion
46
46
  def graph_token
47
47
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
48
48
  rescue StandardError => e
49
- handle_exception(e, level: :debug, operation: 'Chat#graph_token')
49
+ handle_exception(e, level: :warn, operation: 'Chat#graph_token')
50
50
  nil
51
51
  end
52
52
 
@@ -60,7 +60,7 @@ module Legion
60
60
 
61
61
  URI.decode_uri_component(match[1])
62
62
  rescue StandardError => e
63
- handle_exception(e, level: :debug, operation: 'Chat#extract_chat_id', url: url)
63
+ handle_exception(e, level: :warn, operation: 'Chat#extract_chat_id', url: url)
64
64
  nil
65
65
  end
66
66
 
@@ -150,7 +150,7 @@ module Legion
150
150
  " ↳ [#{timestamp}] #{sender}: #{text}"
151
151
  end
152
152
  rescue StandardError => e
153
- handle_exception(e, level: :debug, operation: 'Chat#fetch_reply_lines',
153
+ handle_exception(e, level: :warn, operation: 'Chat#fetch_reply_lines',
154
154
  chat_id: chat_id, message_id: message_id)
155
155
  []
156
156
  end
@@ -51,7 +51,7 @@ module Legion
51
51
  def graph_token
52
52
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
53
53
  rescue StandardError => e
54
- handle_exception(e, level: :debug, operation: 'Meeting#graph_token')
54
+ handle_exception(e, level: :warn, operation: 'Meeting#graph_token')
55
55
  nil
56
56
  end
57
57
 
@@ -75,7 +75,7 @@ module Legion
75
75
  match = uri.path.match(%r{/l/chat/(19:meeting_[^/]+)})
76
76
  match&.[](1)
77
77
  rescue URI::InvalidURIError => e
78
- handle_exception(e, level: :debug, operation: 'Meeting#extract_meeting_thread_id', url: url)
78
+ handle_exception(e, level: :warn, operation: 'Meeting#extract_meeting_thread_id', url: url)
79
79
  nil
80
80
  end
81
81
 
@@ -14,7 +14,7 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Channel)
16
16
  rescue StandardError => e
17
- handle_exception(e, level: :debug, operation: 'AbsorbChannel#enabled?')
17
+ handle_exception(e, level: :warn, operation: 'AbsorbChannel#enabled?')
18
18
  false
19
19
  end
20
20
 
@@ -53,7 +53,7 @@ module Legion
53
53
 
54
54
  data.transform_keys(&:to_sym)
55
55
  rescue StandardError => e
56
- handle_exception(e, level: :debug, operation: 'AbsorbChannel#parse_payload')
56
+ handle_exception(e, level: :warn, operation: 'AbsorbChannel#parse_payload')
57
57
  {}
58
58
  end
59
59
  end
@@ -14,7 +14,7 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Chat)
16
16
  rescue StandardError => e
17
- handle_exception(e, level: :debug, operation: 'AbsorbChat#enabled?')
17
+ handle_exception(e, level: :warn, operation: 'AbsorbChat#enabled?')
18
18
  false
19
19
  end
20
20
 
@@ -53,7 +53,7 @@ module Legion
53
53
 
54
54
  data.transform_keys(&:to_sym)
55
55
  rescue StandardError => e
56
- handle_exception(e, level: :debug, operation: 'AbsorbChat#parse_payload')
56
+ handle_exception(e, level: :warn, operation: 'AbsorbChat#parse_payload')
57
57
  {}
58
58
  end
59
59
  end
@@ -14,7 +14,7 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Meeting)
16
16
  rescue StandardError => e
17
- handle_exception(e, level: :debug, operation: 'AbsorbMeeting#enabled?')
17
+ handle_exception(e, level: :warn, operation: 'AbsorbMeeting#enabled?')
18
18
  false
19
19
  end
20
20
 
@@ -53,7 +53,7 @@ module Legion
53
53
 
54
54
  data.transform_keys(&:to_sym)
55
55
  rescue StandardError => e
56
- handle_exception(e, level: :debug, operation: 'AbsorbMeeting#parse_payload')
56
+ handle_exception(e, level: :warn, operation: 'AbsorbMeeting#parse_payload')
57
57
  {}
58
58
  end
59
59
  end
@@ -24,7 +24,7 @@ module Legion
24
24
  base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
25
25
  [base_delay + 5.0, 14].max
26
26
  rescue StandardError => e
27
- handle_exception(e, level: :debug, operation: 'ApiIngest#delay')
27
+ handle_exception(e, level: :warn, operation: 'ApiIngest#delay')
28
28
  14
29
29
  end
30
30
 
@@ -93,7 +93,7 @@ module Legion
93
93
 
94
94
  Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
95
95
  rescue StandardError => e
96
- handle_exception(e, level: :debug, operation: 'ApiIngest#imprint_active?')
96
+ handle_exception(e, level: :warn, operation: 'ApiIngest#imprint_active?')
97
97
  false
98
98
  end
99
99
  end
@@ -41,7 +41,7 @@ module Legion
41
41
 
42
42
  Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
43
43
  rescue StandardError => e
44
- handle_exception(e, level: :debug, operation: 'CacheBulkIngest#imprint_active?')
44
+ handle_exception(e, level: :warn, operation: 'CacheBulkIngest#imprint_active?')
45
45
  false
46
46
  end
47
47
  end
@@ -34,7 +34,7 @@ module Legion
34
34
  def enabled?
35
35
  settings.dig(:channel_poller, :enabled)
36
36
  rescue StandardError => e
37
- handle_exception(e, level: :debug, operation: 'ChannelPoller#enabled?')
37
+ handle_exception(e, level: :warn, operation: 'ChannelPoller#enabled?')
38
38
  false
39
39
  end
40
40
 
@@ -29,7 +29,7 @@ module Legion
29
29
  defined?(Legion::Extensions::MicrosoftTeams::Runners::Bot) &&
30
30
  Legion.const_defined?(:Transport, false)
31
31
  rescue StandardError => e
32
- handle_exception(e, level: :debug, operation: 'DirectChatPoller#enabled?')
32
+ handle_exception(e, level: :warn, operation: 'DirectChatPoller#enabled?')
33
33
  false
34
34
  end
35
35
 
@@ -22,7 +22,7 @@ module Legion
22
22
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
23
23
  token_available?
24
24
  rescue StandardError => e
25
- handle_exception(e, level: :debug, operation: 'IncrementalSync#enabled?')
25
+ handle_exception(e, level: :warn, operation: 'IncrementalSync#enabled?')
26
26
  false
27
27
  end
28
28
 
@@ -28,7 +28,7 @@ module Legion
28
28
  settings.dig(:meeting_ingest, :enabled) &&
29
29
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
30
30
  rescue StandardError => e
31
- handle_exception(e, level: :debug, operation: 'MeetingIngest#enabled?')
31
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#enabled?')
32
32
  false
33
33
  end
34
34
 
@@ -28,7 +28,7 @@ module Legion
28
28
  defined?(Legion::Extensions::MicrosoftTeams::Runners::Bot) &&
29
29
  Legion.const_defined?(:Transport, false)
30
30
  rescue StandardError => e
31
- handle_exception(e, level: :debug, operation: 'ObservedChatPoller#enabled?')
31
+ handle_exception(e, level: :warn, operation: 'ObservedChatPoller#enabled?')
32
32
  false
33
33
  end
34
34
 
@@ -23,7 +23,7 @@ module Legion
23
23
  settings.dig(:presence_poller, :enabled) &&
24
24
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
25
25
  rescue StandardError => e
26
- handle_exception(e, level: :debug, operation: 'PresencePoller#enabled?')
26
+ handle_exception(e, level: :warn, operation: 'PresencePoller#enabled?')
27
27
  false
28
28
  end
29
29
 
@@ -16,7 +16,7 @@ module Legion
16
16
  base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
17
17
  base_delay + 5.0
18
18
  rescue StandardError => e
19
- handle_exception(e, level: :debug, operation: 'ProfileIngest#delay')
19
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#delay')
20
20
  14.0
21
21
  end
22
22
 
@@ -25,7 +25,7 @@ module Legion
25
25
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
26
26
  token_available?
27
27
  rescue StandardError => e
28
- handle_exception(e, level: :debug, operation: 'ProfileIngest#enabled?')
28
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#enabled?')
29
29
  false
30
30
  end
31
31
 
@@ -92,7 +92,7 @@ module Legion
92
92
  total_wait = 0.0
93
93
  last_advertised = nil
94
94
 
95
- loop do
95
+ (@max_retries + 1).times do
96
96
  response = @app.call(env.dup)
97
97
  return response unless retryable?(response.status)
98
98
 
@@ -12,7 +12,7 @@ module Legion
12
12
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
13
13
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
14
14
 
15
- def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **_opts)
15
+ def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **)
16
16
  token ||= entra_delegated_token
17
17
  ::Faraday.new(url: api_url) do |conn|
18
18
  conn.request :json
@@ -24,7 +24,7 @@ module Legion
24
24
  end
25
25
  end
26
26
 
27
- def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **_opts)
27
+ def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **)
28
28
  ::Faraday.new(url: service_url) do |conn|
29
29
  conn.request :json
30
30
  conn.use Legion::Extensions::MicrosoftTeams::Faraday::ThrottleCircuit, **throttle_circuit_options
@@ -39,7 +39,7 @@ module Legion
39
39
  user_id == 'me' ? 'me' : "users/#{user_id}"
40
40
  end
41
41
 
42
- def oauth_connection(tenant_id: 'common', **_opts)
42
+ def oauth_connection(tenant_id: 'common', **)
43
43
  ::Faraday.new(url: "https://login.microsoftonline.com/#{tenant_id}") do |conn|
44
44
  conn.request :url_encoded
45
45
  conn.response :json, content_type: /\bjson$/
@@ -51,7 +51,7 @@ module Legion
51
51
  def entra_delegated_token
52
52
  Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
53
53
  rescue StandardError => e
54
- handle_exception(e, level: :debug, operation: 'Client#entra_delegated_token')
54
+ handle_exception(e, level: :warn, operation: 'Client#entra_delegated_token')
55
55
  nil
56
56
  end
57
57
 
@@ -21,7 +21,7 @@ module Legion
21
21
  resp.body
22
22
  end
23
23
  rescue StandardError => e
24
- handle_exception(e, level: :debug, operation: 'GraphCache#cached_graph_get', path: path)
24
+ handle_exception(e, level: :warn, operation: 'GraphCache#cached_graph_get', path: path)
25
25
  conn.get(path, params).body
26
26
  end
27
27
 
@@ -35,7 +35,7 @@ module Legion
35
35
  key = graph_cache_key(path: path, params: params, shared: shared)
36
36
  cache_delete(key)
37
37
  rescue StandardError => e
38
- handle_exception(e, level: :debug, operation: 'GraphCache#invalidate_graph_cache')
38
+ handle_exception(e, level: :warn, operation: 'GraphCache#invalidate_graph_cache')
39
39
  end
40
40
 
41
41
  private
@@ -55,7 +55,7 @@ module Legion
55
55
 
56
56
  Legion::Settings[:microsoft_teams] || {}
57
57
  rescue StandardError => e
58
- handle_exception(e, level: :debug, operation: 'GraphCache#teams_extension_settings')
58
+ handle_exception(e, level: :warn, operation: 'GraphCache#teams_extension_settings')
59
59
  {}
60
60
  end
61
61
  end
@@ -21,7 +21,7 @@ module Legion
21
21
  next_link = nil
22
22
  page = 0
23
23
 
24
- loop do
24
+ max_pages.times do
25
25
  current_path = next_link || path
26
26
  data = graph_get(current_path, token: token, params: page.zero? ? params : {})
27
27
  break if data.nil?
@@ -59,7 +59,7 @@ module Legion
59
59
 
60
60
  raw.is_a?(Hash) ? raw : ::JSON.parse(raw, symbolize_names: true)
61
61
  rescue StandardError => e
62
- handle_exception(e, level: :debug, operation: 'HighWaterMark#get_extended_hwm',
62
+ handle_exception(e, level: :warn, operation: 'HighWaterMark#get_extended_hwm',
63
63
  chat_id: chat_id)
64
64
  nil
65
65
  end
@@ -55,7 +55,7 @@ module Legion
55
55
  Legion::Extensions::Mesh::Helpers::PreferenceProfile.preference_instructions(profile: profile)
56
56
  rescue StandardError => e
57
57
  if defined?(handle_exception)
58
- handle_exception(e, level: :debug, operation: 'PromptResolver#preference_instructions_for',
58
+ handle_exception(e, level: :warn, operation: 'PromptResolver#preference_instructions_for',
59
59
  owner_id: owner_id)
60
60
  end
61
61
  nil
@@ -153,7 +153,7 @@ module Legion
153
153
  profile_traces.map { |t| { type: t[:trace_type], content: t[:content_payload].to_s[0, 200] } }
154
154
  rescue StandardError => e
155
155
  if defined?(handle_exception)
156
- handle_exception(e, level: :debug, operation: 'SessionManager#trace_seed_for',
156
+ handle_exception(e, level: :warn, operation: 'SessionManager#trace_seed_for',
157
157
  owner_id: owner_id)
158
158
  end
159
159
  nil
@@ -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)
@@ -22,16 +22,12 @@ module Legion
22
22
 
23
23
  definition :ingest_api, mcp_exposed: false
24
24
 
25
- # Fetch top contacts via /me/people, then pull recent messages from
26
- # their 1:1 chats. Stores each message as an individual memory trace
27
- # (same format as CacheIngest) with dedup by content hash.
28
- #
29
- # Requires a delegated token with Chat.Read and People.Read scopes.
30
25
  def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
31
26
  log.debug("ApiIngest#ingest_api top_people=#{top_people} message_depth=#{message_depth}")
32
27
  return error_result('lex-memory not loaded') unless memory_available?
33
28
  return error_result('no token provided') unless token && !token.empty?
34
29
 
30
+ @fetch_failures = 0
35
31
  restore_hwm_from_traces
36
32
 
37
33
  people = fetch_top_people(token: token, top: top_people)
@@ -104,6 +100,7 @@ module Legion
104
100
 
105
101
  { result: { stored: stored, skipped: skipped, people_ingested: people_ingested,
106
102
  people_found: people.length, chats_found: chats.length,
103
+ fetch_failures: @fetch_failures,
107
104
  apollo: apollo_results } }
108
105
  # rubocop:disable Legion/RescueLogging/NoCapture
109
106
  # Re-raise unlogged: surface the typed throttle to the caller (the
@@ -117,13 +114,15 @@ module Legion
117
114
  # rubocop:enable Legion/RescueLogging/NoCapture
118
115
  rescue StandardError => e
119
116
  handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
120
- { 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 } }
121
119
  end
122
120
 
123
121
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
124
122
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
125
123
 
126
124
  MAX_CHAT_PAGES = 10
125
+ CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
127
126
 
128
127
  private
129
128
 
@@ -135,15 +134,24 @@ module Legion
135
134
  resp = conn.get('me/people', { '$top' => top })
136
135
 
137
136
  log.debug("ApiIngest: fetch_top_people status=#{resp.status} count=#{(resp.body || {}).fetch('value', []).size}")
138
- if resp.status == 403
139
- 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
140
143
  return []
141
144
  end
142
145
 
143
146
  people = (resp.body || {}).fetch('value', [])
144
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
145
152
  rescue StandardError => e
146
153
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_top_people')
154
+ @fetch_failures = (@fetch_failures || 0) + 1
147
155
  []
148
156
  end
149
157
 
@@ -154,8 +162,17 @@ module Legion
154
162
  params = { '$top' => 50 }
155
163
  pages = 0
156
164
 
157
- loop do
165
+ MAX_CHAT_PAGES.times do
158
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
+
159
176
  body = resp.body || {}
160
177
  chats = body.fetch('value', [])
161
178
  all_chats.concat(chats)
@@ -163,7 +180,6 @@ module Legion
163
180
 
164
181
  next_link = body['@odata.nextLink']
165
182
  break unless next_link
166
- break if pages >= MAX_CHAT_PAGES
167
183
 
168
184
  url = next_link
169
185
  params = {}
@@ -173,8 +189,13 @@ module Legion
173
189
  filtered = all_chats.select { |c| allowed_types.include?(c['chatType']) }
174
190
  log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{filtered.size} eligible (1:1/group/meeting)")
175
191
  filtered
192
+ # rubocop:disable Legion/RescueLogging/NoCapture
193
+ rescue Errors::Throttled
194
+ raise
195
+ # rubocop:enable Legion/RescueLogging/NoCapture
176
196
  rescue StandardError => e
177
197
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_one_on_one_chats')
198
+ @fetch_failures = (@fetch_failures || 0) + 1
178
199
  []
179
200
  end
180
201
 
@@ -183,9 +204,10 @@ module Legion
183
204
  by_user_id = {}
184
205
  by_name = {}
185
206
 
186
- chats.each do |chat|
207
+ sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
208
+ sorted.each do |chat|
187
209
  members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
188
- shared: true)
210
+ shared: true, ttl: members_cache_ttl)
189
211
  .then { |body| (body || {}).fetch('value', []) }
190
212
  members.each do |m|
191
213
  email = m['email']&.downcase
@@ -201,8 +223,13 @@ module Legion
201
223
  end
202
224
 
203
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
204
230
  rescue StandardError => e
205
231
  handle_exception(e, level: :warn, operation: 'ApiIngest#build_chat_member_index')
232
+ @fetch_failures = (@fetch_failures || 0) + 1
206
233
  { email: {}, user_id: {}, name: {} }
207
234
  end
208
235
 
@@ -216,17 +243,41 @@ module Legion
216
243
  chat_index[:name][display_name]
217
244
  end
218
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
+
219
255
  def fetch_chat_messages(conn:, chat_id:, depth: 50)
220
256
  hwm = get_extended_hwm(chat_id: chat_id)
221
257
  params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
222
- params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
223
258
 
224
259
  resp = conn.get("chats/#{chat_id}/messages", params)
225
- log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{(resp.body || {}).fetch('value', []).size}")
226
- (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
227
277
  rescue StandardError => e
228
278
  handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_chat_messages',
229
279
  chat_id: chat_id)
280
+ @fetch_failures = (@fetch_failures || 0) + 1
230
281
  []
231
282
  end
232
283
 
@@ -295,7 +346,7 @@ module Legion
295
346
  end
296
347
  hashes
297
348
  rescue StandardError => e
298
- handle_exception(e, level: :debug, operation: 'ApiIngest#load_existing_hashes')
349
+ handle_exception(e, level: :warn, operation: 'ApiIngest#load_existing_hashes')
299
350
  Set.new
300
351
  end
301
352
 
@@ -324,12 +375,12 @@ module Legion
324
375
  trace_ids.each_cons(2) do |id_a, id_b|
325
376
  store.record_coactivation(id_a, id_b)
326
377
  rescue StandardError => e
327
- handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces',
378
+ handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces',
328
379
  id_a: id_a, id_b: id_b)
329
380
  end
330
381
  end
331
382
  rescue StandardError => e
332
- handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces')
383
+ handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces')
333
384
  end
334
385
 
335
386
  def publish_to_apollo(person_texts)
@@ -388,7 +439,7 @@ module Legion
388
439
 
389
440
  { success: true, count: result[:entities].length }
390
441
  rescue StandardError => e
391
- handle_exception(e, level: :debug, operation: 'ApiIngest#extract_and_ingest_entities',
442
+ handle_exception(e, level: :warn, operation: 'ApiIngest#extract_and_ingest_entities',
392
443
  person_name: person_name)
393
444
  { success: false, count: 0 }
394
445
  end
@@ -407,6 +458,16 @@ module Legion
407
458
  @apollo_knowledge_runner ||= Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
408
459
  end
409
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
+
410
471
  def error_result(message)
411
472
  { result: { stored: 0, skipped: 0, error: message } }
412
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'digest'
4
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'
@@ -52,29 +53,12 @@ module Legion
52
53
  conn = graph_connection(token: token)
53
54
  profile = conn.get('me').body
54
55
 
55
- memory_runner.store_trace(
56
- type: :identity,
57
- content_payload: ::JSON.dump(profile),
58
- domain_tags: %w[teams self owner],
59
- confidence: 1.0,
60
- origin: :direct_experience
61
- )
62
-
63
56
  presence = begin
64
57
  conn.get('me/presence').body
65
58
  rescue StandardError => e
66
- handle_exception(e, level: :debug, operation: 'ProfileIngest#ingest_self presence') if defined?(handle_exception)
59
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#ingest_self presence') if defined?(handle_exception)
67
60
  {}
68
61
  end
69
- unless presence.empty?
70
- memory_runner.store_trace(
71
- type: :sensory,
72
- content_payload: ::JSON.dump(presence),
73
- domain_tags: %w[teams presence self],
74
- confidence: 0.8,
75
- origin: :direct_experience
76
- )
77
- end
78
62
 
79
63
  { profile: profile, presence: presence }
80
64
  # rubocop:disable Legion/RescueLogging/NoCapture
@@ -103,18 +87,6 @@ module Legion
103
87
  people = (resp.body || {}).fetch('value', [])
104
88
  people.sort_by! { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
105
89
 
106
- people.each do |person|
107
- name = person['displayName'] || 'Unknown'
108
- memory_runner.store_trace(
109
- type: :semantic,
110
- content_payload: ::JSON.dump(person.slice('displayName', 'jobTitle', 'department',
111
- 'officeLocation', 'scoredEmailAddresses')),
112
- domain_tags: ['teams', 'peer', "peer:#{name}"],
113
- confidence: 0.7,
114
- origin: :direct_experience
115
- )
116
- end
117
-
118
90
  { people: people, count: people.length }
119
91
  # rubocop:disable Legion/RescueLogging/NoCapture
120
92
  # Re-raise unlogged: full_ingest logs the throttle once and aborts
@@ -248,13 +220,16 @@ module Legion
248
220
  top_people: top_people, message_depth: message_depth)
249
221
  end
250
222
 
223
+ CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
224
+
251
225
  private
252
226
 
253
227
  def build_chat_member_index(conn:, chats:)
254
228
  index = {}
255
- chats.each do |chat|
229
+ sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
230
+ sorted.each do |chat|
256
231
  members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
257
- shared: true)
232
+ shared: true, ttl: members_cache_ttl)
258
233
  .then { |body| (body || {}).fetch('value', []) }
259
234
  members.each do |m|
260
235
  email = m['email']&.downcase
@@ -273,13 +248,32 @@ module Legion
273
248
  {}
274
249
  end
275
250
 
251
+ def members_cache_ttl
252
+ return @members_cache_ttl if defined?(@members_cache_ttl)
253
+
254
+ @members_cache_ttl = if respond_to?(:settings, true) && settings.respond_to?(:dig)
255
+ settings.dig(:cache, :members_ttl) || 86_400
256
+ else
257
+ 86_400
258
+ end
259
+ end
260
+
276
261
  def fetch_new_messages(conn:, chat_id:, depth: 50)
277
262
  hwm = get_extended_hwm(chat_id: chat_id)
278
263
  params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
279
- params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
280
264
 
281
265
  resp = conn.get("chats/#{chat_id}/messages", params)
282
- (resp.body || {}).fetch('value', [])
266
+
267
+ unless (200..299).cover?(resp.status)
268
+ error_code = resp.body&.dig('error', 'code')
269
+ log.warn('[microsoft_teams][profile_ingest] fetch_new_messages non-2xx: ' \
270
+ "chat_id=#{chat_id} status=#{resp.status} error_code=#{error_code}")
271
+ @fetch_failures = (@fetch_failures || 0) + 1
272
+ return []
273
+ end
274
+
275
+ messages = (resp.body || {}).fetch('value', [])
276
+ hwm_client_side_cut(messages: messages, hwm: hwm)
283
277
  # rubocop:disable Legion/RescueLogging/NoCapture
284
278
  # Re-raise unlogged: propagate to full_ingest so the per-chat
285
279
  # fan-out stops on the first throttle; full_ingest logs it once.
@@ -289,9 +283,19 @@ module Legion
289
283
  rescue StandardError => e
290
284
  handle_exception(e, level: :warn, operation: 'ProfileIngest#fetch_new_messages',
291
285
  chat_id: chat_id)
286
+ @fetch_failures = (@fetch_failures || 0) + 1
292
287
  []
293
288
  end
294
289
 
290
+ def hwm_client_side_cut(messages:, hwm:)
291
+ return messages unless hwm&.dig(:last_message_at)
292
+
293
+ cutoff = hwm[:last_message_at]
294
+ seen = Set.new
295
+ messages.take_while { |m| m['createdDateTime'] && m['createdDateTime'] > cutoff }
296
+ .select { |m| seen.add?(m['id'] || Digest::SHA256.hexdigest(m.dig('body', 'content').to_s)[0, 16]) }
297
+ end
298
+
295
299
  def extract_conversation(messages:, peer_name:)
296
300
  return nil if messages.empty?
297
301
 
@@ -315,7 +319,7 @@ module Legion
315
319
  llm_ask(message: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
316
320
  end
317
321
  rescue StandardError => e
318
- handle_exception(e, level: :debug, operation: 'ProfileIngest#extract_conversation')
322
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#extract_conversation')
319
323
  nil
320
324
  end
321
325
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.6.52'
6
+ VERSION = '0.6.53'
7
7
  end
8
8
  end
9
9
  end
@@ -107,7 +107,8 @@ module Legion
107
107
  interval: 60
108
108
  },
109
109
  cache: {
110
- graph_ttl: 300
110
+ graph_ttl: 300,
111
+ members_ttl: 86_400
111
112
  },
112
113
  client: {
113
114
  throttle_circuit: {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-microsoft_teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.52
4
+ version: 0.6.53
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity