legion-gaia 0.9.63 → 0.9.66

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: d865d2e228e487153ee4681a3d0a939c83090e236f58e99cff670b351d5cc07e
4
- data.tar.gz: 84252e97bc38980791c4ac74aaedf1fcc7bc00d6de30c8da7d4fce98d37a89a1
3
+ metadata.gz: dc0229532bc53d2868fac7cc6279bb2875ceaaedbb63269dc8b5a742a7c95832
4
+ data.tar.gz: da7e11d18691d6af5e90e2c4da209feaf1b6ab79b4aaa91f4c409e517d7428cf
5
5
  SHA512:
6
- metadata.gz: 4b307e1dfcf3cfba5e3e0378209c4d562578de7085753f216ed6a3d45e969b3d0e92724f6e428d6635dad31d7d19bdbf5f451b7f0e21406492dca09b97a0f1e0
7
- data.tar.gz: 987ad87a47603e4c65cefaad42b26f8817b65154c070040c84936694e7763e62492664c7bb87305d5d03b6e6e6ce4e7d9ab941290f0f41f251011251968811c1
6
+ metadata.gz: c87775e2f41b815da9b996ceca3383dbf956ec8c8e643c26ec8390fb7477fc0ead9739b4802cbdc5984ebf1266f1d5dd22d145c7b657cc8e7712035f0496932c
7
+ data.tar.gz: e03a41455d7d36dbb49d89e2b7c74e3db930c07a1a730cb4c695a088c446be020463ff0ea588bdea1ce01b06504890890afa08c42344fbd16e71b6818058727b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.66] - 2026-07-24
4
+ ### Added
5
+ - `Gaia.observe_from_pipeline(identity:, caller:, exchange_id:)` — pipeline observation for API clients; reinforces bond, triggers coldstart, registers duckling partner on first human request
6
+ - Bounded idempotency (FIFO hash, 10k max) prevents duplicate observations per exchange_id
7
+ - Only fires for `:human` type callers; system/internal calls are ignored
8
+
9
+ ## [0.9.65] - 2026-07-23
10
+ ### Fixed
11
+ - `resolve_process_identity` now uses `Legion::Identity::Process.canonical_name` exclusively — never `Etc.loginname` or `ENV['USER']` (spoofable, wrong string in corporate environments)
12
+ - Provisional partner registration skips gracefully when identity hasn't resolved yet (no registration under wrong/anonymous string)
13
+ - `drain_growth_frames` moved to public API (Advisory calls it directly)
14
+ - Removed `private_class_method` from Advisory — all methods callable
15
+
16
+ ### Added
17
+ - Advisory now produces `:system_prompt` with partner model slots, growth frames, and epistemic qualifiers — injected via existing legion-llm `gaia_advisory` step (zero legion-llm changes)
18
+ - `Advisory.build_partner_system_prompt(identity:)` — assembles working-memory context for the LLM
19
+ - `Advisory.build_partner_slots(identity:)` — renders PartnerModel slots as natural language
20
+ - `Advisory.drain_growth_content` — drains and renders pending milestone/growth frames
21
+ - `Advisory.build_epistemic_qualifier(identity:)` — delegates to VisibleGrowth for uncertainty markers
22
+ - Log line: `[gaia] advisory partner_prompt injected identity=X length=N` when partner context fires
23
+
24
+ ## [0.9.64] - 2026-07-22
25
+ ### Added
26
+ - H9: `Disclosure.report(identity:)` — full partner model transparency surface; includes bond_state, all behavioral_synapses with autonomy_mode, preferences, calibration_weights, imprint_state, prediction_accuracy; each section soft-guarded (nil if extension not loaded, no crash); always includes data_locality statement and termination_available: true
27
+ - H9: `VisibleGrowth.milestone_acknowledgment(identity:, domain:, new_mode:, old_mode:)` — fires once per tier transition per (identity, domain); returns natural-language acknowledgment ("I've noticed you prefer shorter answers — I'm now adjusting my approach.")
28
+ - H9: `VisibleGrowth.pain_revert_acknowledgment(domain:)` — called after 3 consecutive failures reset a synapse; "I've been getting [domain] wrong — I've reset that."
29
+ - H9: `VisibleGrowth.graduation_acknowledgment(identity:)` — fires once per identity when imprint window closes
30
+ - H9: `VisibleGrowth.onboarding_frame(identity:)` — first-contact transparency; plain language about learning, local storage, and termination option; fires once per identity
31
+ - H9: `VisibleGrowth.epistemic_qualifier(identity:, domain:)` — returns hedge/qualifier string during imprint or when relevant synapse is observe-tier; nil when confident (default post-imprint)
32
+ - Wire milestone and pain surfaces into `grade_behavioral_synapses`; pending frames stored in `@pending_growth_frames` (drain via `Gaia.drain_growth_frames`)
33
+ - `GET /api/gaia/partner/:identity/report` route — returns full Disclosure.report for any identity
34
+
3
35
  ## [0.9.63] - 2026-07-22
4
36
  ### Added
5
37
  - H8: `DeathProtocol.terminate_bond(identity:, confirm:)` — single entry point for bond termination; walks cascade across all stores (bond, behavioral_synapses, session, audit, attribution, calibration, preferences, memory_traces, predictions, trust); returns proof-of-destruction receipt with per-store results; emits `gaia.bond.terminated` event
@@ -20,6 +20,14 @@ module Legion
20
20
  advisory[:valence] = Gaia.last_valences if Gaia.last_valences
21
21
  merge_tick_data!(advisory, Gaia.registry&.tick_host&.last_tick_result)
22
22
  merge_observer_data!(advisory, caller)
23
+
24
+ identity = caller&.dig(:requested_by, :identity)
25
+ partner_prompt = build_partner_system_prompt(identity: identity) if identity
26
+ if partner_prompt
27
+ advisory[:system_prompt] = partner_prompt
28
+ log.info("[gaia] advisory partner_prompt injected identity=#{identity} length=#{partner_prompt.length}")
29
+ end
30
+
23
31
  result = advisory.compact
24
32
  if result.any?
25
33
  log.info("GAIA advisory generated conversation_id=#{conversation_id} keys=#{result.keys.join(',')}")
@@ -94,8 +102,58 @@ module Legion
94
102
  nil
95
103
  end
96
104
 
97
- private_class_method :merge_tick_data!, :apply_tool_hints!, :apply_suppress!, :merge_observer_data!,
98
- :normalize_routing_hint, :value_for
105
+ def build_partner_system_prompt(identity:)
106
+ log.unknown("[gaia] build_partner_system_prompt identity=#{identity} " \
107
+ "bond_registry_defined=#{defined?(Legion::Gaia::BondRegistry)} " \
108
+ "partner=#{defined?(Legion::Gaia::BondRegistry) && BondRegistry.partner?(identity.to_s)}")
109
+
110
+ return nil unless defined?(Legion::Gaia::BondRegistry) && BondRegistry.partner?(identity.to_s)
111
+
112
+ parts = ['|gaia:active|']
113
+
114
+ slots = build_partner_slots(identity: identity)
115
+ parts << slots if slots
116
+ log.unknown("[gaia] partner_slots count=#{slots ? 'present' : 'empty'}")
117
+
118
+ growth = drain_growth_content
119
+ parts << growth if growth
120
+
121
+ qualifier = build_epistemic_qualifier(identity: identity)
122
+ parts << qualifier if qualifier
123
+
124
+ parts.join("\n\n")
125
+ end
126
+
127
+ def build_partner_slots(identity:)
128
+ return nil unless defined?(Legion::Gaia::PartnerModel)
129
+
130
+ slots = PartnerModel.build(identity: identity)
131
+ return nil if slots.empty?
132
+
133
+ lines = slots.map do |slot|
134
+ domain = slot[:domain]
135
+ content = slot[:content]
136
+ content_str = content.is_a?(Hash) ? content.map { |k, v| "#{k}: #{v}" }.join(', ') : content.to_s
137
+ "- #{domain}: #{content_str}"
138
+ end
139
+
140
+ "What I know about working with you:\n#{lines.join("\n")}"
141
+ end
142
+
143
+ def drain_growth_content
144
+ return nil unless Gaia.respond_to?(:drain_growth_frames)
145
+
146
+ frames = Gaia.drain_growth_frames
147
+ return nil if frames.nil? || frames.empty?
148
+
149
+ frames.join("\n")
150
+ end
151
+
152
+ def build_epistemic_qualifier(identity:)
153
+ return nil unless defined?(Legion::Gaia::VisibleGrowth)
154
+
155
+ VisibleGrowth.epistemic_qualifier(identity: identity)
156
+ end
99
157
  end
100
158
  end
101
159
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Gaia
5
+ module Disclosure
6
+ module_function
7
+
8
+ # Full partner model report — what the agent knows, how it was earned, what stays local.
9
+ # Principal-bound: caller reads ONLY their own partner model.
10
+ #
11
+ # @param identity [String] the partner identity key
12
+ # @return [Hash]
13
+ def report(identity:)
14
+ identity_str = identity.to_s
15
+
16
+ {
17
+ identity: identity_str,
18
+ bond_state: fetch_bond_state(identity_str),
19
+ behavioral_synapses: fetch_behavioral_synapses(identity_str),
20
+ preferences: fetch_preferences(identity_str),
21
+ calibration_weights: fetch_calibration_weights(identity_str),
22
+ imprint_state: fetch_imprint_state,
23
+ prediction_accuracy: fetch_prediction_accuracy(identity_str),
24
+ data_locality: 'All data is stored locally on this node. Nothing leaves this machine.',
25
+ termination_available: true
26
+ }
27
+ end
28
+
29
+ # --- private section ---
30
+
31
+ def fetch_bond_state(identity)
32
+ entry = Legion::Gaia::BondRegistry.instance_variable_get(:@bonds)[identity]
33
+ return nil unless entry
34
+
35
+ {
36
+ bond: entry[:bond],
37
+ strength: entry[:strength],
38
+ origin: entry[:origin],
39
+ reinforcement_count: entry[:reinforcement_count],
40
+ since: entry[:since],
41
+ preferred_channel: entry[:preferred_channel],
42
+ lifecycle_state: Legion::Gaia::BondRegistry.bond_state(identity)
43
+ }
44
+ rescue StandardError => e
45
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_bond_state')
46
+ nil
47
+ end
48
+
49
+ def fetch_behavioral_synapses(identity)
50
+ all = Legion::Gaia::BehavioralSynapse.all_for(identity: identity)
51
+ return nil if all.empty?
52
+
53
+ all.map do |entry|
54
+ {
55
+ id: entry[:id],
56
+ domain: entry[:domain],
57
+ directive: entry[:directive],
58
+ confidence: entry[:confidence],
59
+ autonomy_mode: Legion::Gaia::BehavioralSynapse::Math.autonomy_mode(entry[:confidence].to_f),
60
+ status: entry[:status],
61
+ origin: entry[:origin],
62
+ consecutive_successes: entry[:consecutive_successes],
63
+ consecutive_failures: entry[:consecutive_failures],
64
+ last_reinforced_at: entry[:last_reinforced_at]
65
+ }
66
+ end
67
+ rescue StandardError => e
68
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_behavioral_synapses')
69
+ nil
70
+ end
71
+
72
+ def fetch_preferences(identity)
73
+ return nil unless defined?(Legion::Extensions::Mesh::Helpers::PreferenceProfile)
74
+
75
+ result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_owner(owner_id: identity)
76
+ return nil unless result.is_a?(Hash) && result[:directives].is_a?(Array)
77
+
78
+ result[:directives]
79
+ rescue StandardError => e
80
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_preferences')
81
+ nil
82
+ end
83
+
84
+ def fetch_calibration_weights(identity)
85
+ return nil unless defined?(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
86
+
87
+ runner = Object.new
88
+ runner.extend(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
89
+ result = runner.respond_to?(:calibration_weights_for) ? runner.calibration_weights_for(identity: identity) : nil
90
+ result.is_a?(Hash) ? result : nil
91
+ rescue StandardError => e
92
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_calibration_weights')
93
+ nil
94
+ end
95
+
96
+ def fetch_imprint_state
97
+ return nil unless defined?(Legion::Extensions::Coldstart)
98
+ return nil unless Legion::Extensions::Coldstart.respond_to?(:connected?) &&
99
+ Legion::Extensions::Coldstart.connected?
100
+
101
+ bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
102
+ {
103
+ layer: bootstrap.respond_to?(:current_layer) ? bootstrap.current_layer : nil,
104
+ observations_count: bootstrap.respond_to?(:observation_count) ? bootstrap.observation_count : nil,
105
+ active: bootstrap.respond_to?(:imprint_active?) ? bootstrap.imprint_active? : nil
106
+ }
107
+ rescue StandardError => e
108
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_imprint_state')
109
+ nil
110
+ end
111
+
112
+ def fetch_prediction_accuracy(identity)
113
+ return nil unless defined?(Legion::Extensions::AgenticInference)
114
+ return nil unless Legion::Extensions::AgenticInference.respond_to?(:accuracy_for)
115
+
116
+ Legion::Extensions::AgenticInference.accuracy_for(identity: identity)
117
+ rescue StandardError => e
118
+ Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_prediction_accuracy')
119
+ nil
120
+ end
121
+
122
+ def handle_exception_quietly(exception, operation)
123
+ return unless defined?(Legion::Logging)
124
+
125
+ Legion::Logging.debug("[gaia] #{operation} soft-guarded: #{exception.class}: #{exception.message}")
126
+ end
127
+
128
+ module_function :fetch_bond_state, :fetch_behavioral_synapses, :fetch_preferences,
129
+ :fetch_calibration_weights, :fetch_imprint_state, :fetch_prediction_accuracy,
130
+ :handle_exception_quietly
131
+ end
132
+ end
133
+ end
@@ -63,6 +63,7 @@ module Legion
63
63
  register_teams_webhook_route(app)
64
64
  register_ingest_route(app)
65
65
  register_bond_terminate_route(app)
66
+ register_partner_report_route(app)
66
67
  end
67
68
 
68
69
  def self.register_status_route(app)
@@ -204,10 +205,20 @@ module Legion
204
205
  end
205
206
  end
206
207
 
208
+ def self.register_partner_report_route(app)
209
+ app.get '/api/gaia/partner/:identity/report' do
210
+ identity = params[:identity]
211
+ halt 400, json_error('missing_identity', 'identity is required') if identity.to_s.empty?
212
+
213
+ result = Legion::Gaia::Disclosure.report(identity: identity)
214
+ json_response(result)
215
+ end
216
+ end
217
+
207
218
  class << self
208
219
  private :register_status_route, :register_ticks_route, :register_channels_route,
209
220
  :register_buffer_route, :register_sessions_route, :register_teams_webhook_route,
210
- :register_ingest_route, :register_bond_terminate_route
221
+ :register_ingest_route, :register_bond_terminate_route, :register_partner_report_route
211
222
  end
212
223
  end
213
224
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Gaia
5
- VERSION = '0.9.63'
5
+ VERSION = '0.9.66'
6
6
  end
7
7
  end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Gaia
5
+ module VisibleGrowth
6
+ module_function
7
+
8
+ # In-memory deduplication sets — reset on restart is intentional (worst case: fires twice)
9
+ @acknowledged_milestones = Set.new
10
+ @graduated_identities = Set.new
11
+ @onboarded_identities = Set.new
12
+ @mutex = Mutex.new
13
+
14
+ TIER_LABELS = {
15
+ observe: 'noticing patterns',
16
+ filter: 'adjusting my approach',
17
+ transform: 'actively shaping my responses',
18
+ autonomous: 'acting on this without asking'
19
+ }.freeze
20
+
21
+ DOMAIN_PHRASES = {
22
+ 'brevity' => 'shorter answers',
23
+ 'verbosity' => 'longer, more detailed answers',
24
+ 'tone' => 'the tone',
25
+ 'format' => 'the format',
26
+ 'code' => 'code style',
27
+ 'detail' => 'the level of detail'
28
+ }.freeze
29
+
30
+ # Called when a synapse confidence crosses into a new autonomy tier.
31
+ # Fires ONCE per (identity, domain, tier transition).
32
+ #
33
+ # @param identity [String]
34
+ # @param domain [String]
35
+ # @param new_mode [Symbol] :observe | :filter | :transform | :autonomous
36
+ # @param old_mode [Symbol]
37
+ # @return [String, nil] acknowledgment message, or nil if already fired
38
+ def milestone_acknowledgment(identity:, domain:, new_mode:, old_mode:)
39
+ key = "#{identity}:#{domain}:#{old_mode}->#{new_mode}"
40
+ @mutex.synchronize do
41
+ return nil if @acknowledged_milestones.include?(key)
42
+
43
+ @acknowledged_milestones.add(key)
44
+ end
45
+
46
+ phrase = domain_phrase(domain)
47
+ tier_desc = TIER_LABELS.fetch(new_mode.to_sym, new_mode.to_s)
48
+
49
+ "I've noticed you prefer #{phrase} — I'm now #{tier_desc}. Tell me if I've got that wrong."
50
+ rescue StandardError => e
51
+ VisibleGrowth.log_quietly(e, 'visible_growth.milestone_acknowledgment')
52
+ nil
53
+ end
54
+
55
+ # Called when pain fires (3 consecutive failures → dampened status).
56
+ #
57
+ # @param identity [String] (unused, reserved for future per-identity routing)
58
+ # @param domain [String]
59
+ # @return [String]
60
+ def pain_revert_acknowledgment(domain:, identity: nil) # rubocop:disable Lint/UnusedMethodArgument
61
+ phrase = domain_phrase(domain)
62
+ "I've been getting #{phrase} wrong — I've reset that. What works for you?"
63
+ rescue StandardError => e
64
+ VisibleGrowth.log_quietly(e, 'visible_growth.pain_revert_acknowledgment')
65
+ "I reset something that wasn't working — what would you prefer?"
66
+ end
67
+
68
+ # Called at imprint graduation (imprint window closes). Fires once per identity.
69
+ #
70
+ # @param identity [String]
71
+ # @return [String, nil]
72
+ def graduation_acknowledgment(identity:)
73
+ @mutex.synchronize do
74
+ return nil if @graduated_identities.include?(identity.to_s)
75
+
76
+ @graduated_identities.add(identity.to_s)
77
+ end
78
+
79
+ "I feel like I know how you work now — I'll ask less and do more. Correct me if I drift."
80
+ rescue StandardError => e
81
+ VisibleGrowth.log_quietly(e, 'visible_growth.graduation_acknowledgment')
82
+ nil
83
+ end
84
+
85
+ # Onboarding honesty — first frame at imprint open. Plain language about what's happening.
86
+ # Fires once per identity.
87
+ #
88
+ # @param identity [String]
89
+ # @return [String, nil] content string for OutputFrame, or nil if already sent
90
+ def onboarding_frame(identity:)
91
+ @mutex.synchronize do
92
+ return nil if @onboarded_identities.include?(identity.to_s)
93
+
94
+ @onboarded_identities.add(identity.to_s)
95
+ end
96
+
97
+ build_onboarding_text(identity: identity)
98
+ rescue StandardError => e
99
+ VisibleGrowth.log_quietly(e, 'visible_growth.onboarding_frame')
100
+ nil
101
+ end
102
+
103
+ # Epistemic honesty surface — returns a hedge/qualifier to append to a response, or nil.
104
+ # Returns nil when confident (should be most of the time post-imprint).
105
+ #
106
+ # @param identity [String]
107
+ # @param domain [String, nil] current exchange domain, if known
108
+ # @return [String, nil]
109
+ def epistemic_qualifier(identity:, domain: nil)
110
+ imprint_active = imprint_active_for?
111
+ synapse = domain ? Legion::Gaia::BehavioralSynapse.for(identity: identity.to_s, domain: domain.to_s) : nil
112
+
113
+ return imprint_qualifier if imprint_active
114
+ return observe_qualifier(domain) if synapse_observe_tier?(synapse)
115
+
116
+ nil
117
+ rescue StandardError => e
118
+ VisibleGrowth.log_quietly(e, 'visible_growth.epistemic_qualifier')
119
+ nil
120
+ end
121
+
122
+ # Reset in-memory state (for tests)
123
+ def reset!
124
+ @mutex.synchronize do
125
+ @acknowledged_milestones = Set.new
126
+ @graduated_identities = Set.new
127
+ @onboarded_identities = Set.new
128
+ end
129
+ end
130
+
131
+ # --- private helpers ---
132
+
133
+ def domain_phrase(domain)
134
+ DOMAIN_PHRASES.fetch(domain.to_s, domain.to_s.tr('_', ' '))
135
+ end
136
+
137
+ def imprint_active_for?
138
+ return false unless defined?(Legion::Extensions::Coldstart)
139
+ return false unless Legion::Extensions::Coldstart.respond_to?(:connected?) &&
140
+ Legion::Extensions::Coldstart.connected?
141
+
142
+ bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
143
+ bootstrap.respond_to?(:imprint_active?) && bootstrap.imprint_active?
144
+ rescue StandardError
145
+ false
146
+ end
147
+
148
+ def imprint_observation_count
149
+ return nil unless defined?(Legion::Extensions::Coldstart)
150
+
151
+ bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
152
+ bootstrap.respond_to?(:observation_count) ? bootstrap.observation_count : nil
153
+ rescue StandardError
154
+ nil
155
+ end
156
+
157
+ def imprint_qualifier
158
+ count = imprint_observation_count
159
+ return "I'm still figuring out how you like things — was this right?" if count.nil? || count < 5
160
+
161
+ "I'm still learning your style — let me know if this missed."
162
+ end
163
+
164
+ def synapse_observe_tier?(synapse)
165
+ return false unless synapse.is_a?(Hash)
166
+
167
+ Legion::Gaia::BehavioralSynapse::Math.autonomy_mode(synapse[:confidence].to_f) == :observe
168
+ end
169
+
170
+ def observe_qualifier(domain)
171
+ "I have a hunch about #{domain_phrase(domain)} — tell me if I'm off."
172
+ end
173
+
174
+ def build_onboarding_text(identity:)
175
+ lines = [
176
+ "I'll be learning as we work together — what you prefer, how you like information, " \
177
+ 'what lands and what misses.',
178
+ 'Everything stays on this machine — nothing is sent anywhere else.',
179
+ 'You can end this at any time — your data goes with it, completely.'
180
+ ]
181
+
182
+ bond = Legion::Gaia::BondRegistry.bond(identity.to_s)
183
+ lines << "You're the partner on this node, so I'll pay closer attention." if bond == :partner
184
+
185
+ lines.join(' ')
186
+ rescue StandardError => e
187
+ VisibleGrowth.log_quietly(e, 'visible_growth.build_onboarding_text')
188
+ "I'm learning as we go. Everything stays local. You can end this any time."
189
+ end
190
+
191
+ def log_quietly(exception, operation)
192
+ return unless defined?(Legion::Logging)
193
+
194
+ Legion::Logging.debug("[gaia] #{operation} rescued: #{exception.class}: #{exception.message}")
195
+ end
196
+
197
+ module_function :domain_phrase, :imprint_active_for?, :imprint_observation_count,
198
+ :imprint_qualifier, :synapse_observe_tier?, :observe_qualifier,
199
+ :build_onboarding_text, :log_quietly
200
+ end
201
+ end
202
+ end
data/lib/legion/gaia.rb CHANGED
@@ -35,6 +35,8 @@ require 'legion/gaia/bond_registry'
35
35
  require 'legion/gaia/death_protocol'
36
36
  require 'legion/gaia/behavioral_synapse'
37
37
  require 'legion/gaia/partner_model'
38
+ require 'legion/gaia/disclosure'
39
+ require 'legion/gaia/visible_growth'
38
40
  require 'legion/gaia/tracker_persistence'
39
41
  require 'legion/gaia/router'
40
42
 
@@ -128,6 +130,7 @@ module Legion
128
130
  @started_at = nil
129
131
  @active_heartbeats = 0
130
132
  @quiescing_phase_handlers_cache = nil
133
+ @pending_growth_frames = nil
131
134
 
132
135
  log.info('Legion::Gaia shut down')
133
136
  end
@@ -327,6 +330,71 @@ module Legion
327
330
  base_status.merge(router_status)
328
331
  end
329
332
 
333
+ def drain_growth_frames
334
+ frames = @pending_growth_frames || []
335
+ @pending_growth_frames = []
336
+ frames
337
+ end
338
+
339
+ def observe_from_pipeline(identity:, caller:, exchange_id:)
340
+ return unless started?
341
+
342
+ caller_type = caller&.dig(:requested_by, :type)
343
+ return unless caller_type == :human
344
+
345
+ identity_str = identity.to_s
346
+ return if identity_str.empty?
347
+ return if identity_lifecycle_blocked?(identity_str)
348
+ return if pipeline_observation_seen?(exchange_id)
349
+
350
+ mark_pipeline_observation(exchange_id)
351
+
352
+ if BondRegistry.bond(identity_str) == :unknown
353
+ if BondRegistry.partner_entry.nil?
354
+ BondRegistry.register(identity_str, bond: :partner, priority: :primary,
355
+ origin: :provisional,
356
+ strength: settings&.dig(:partner, :prior_strength) || 0.5)
357
+ log.info("[gaia] pipeline duckling bond identity=#{identity_str}")
358
+ else
359
+ BondRegistry.register(identity_str, bond: nil)
360
+ end
361
+ end
362
+
363
+ BondRegistry.reinforce(identity_str, multiplier: fetch_imprint_multiplier)
364
+
365
+ if defined?(Legion::Extensions::Coldstart)
366
+ bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
367
+ bootstrap.record_observation
368
+ end
369
+
370
+ log.debug("[gaia] pipeline observation identity=#{identity_str} exchange_id=#{exchange_id}")
371
+ rescue StandardError => e
372
+ handle_exception(e, level: :warn, operation: 'gaia.observe_from_pipeline', identity: identity)
373
+ end
374
+
375
+ def pipeline_observation_seen?(exchange_id)
376
+ return false if exchange_id.nil?
377
+
378
+ @pipeline_observations ||= {}
379
+ @pipeline_observations.key?(exchange_id)
380
+ end
381
+
382
+ def mark_pipeline_observation(exchange_id)
383
+ return if exchange_id.nil?
384
+
385
+ @pipeline_observations ||= {}
386
+ @pipeline_observation_order ||= []
387
+
388
+ @pipeline_observations[exchange_id] = true
389
+ @pipeline_observation_order << exchange_id
390
+
391
+ max_size = settings&.dig(:pipeline_observation, :max_tracked) || 10_000
392
+ while @pipeline_observation_order.size > max_size
393
+ oldest = @pipeline_observation_order.shift
394
+ @pipeline_observations.delete(oldest)
395
+ end
396
+ end
397
+
330
398
  private
331
399
 
332
400
  def heartbeat_mutex
@@ -582,14 +650,22 @@ module Legion
582
650
  true
583
651
  end
584
652
 
585
- def observe_interlocutor(input_frame, identity)
653
+ def observe_interlocutor(input_frame, identity) # rubocop:disable Metrics/AbcSize
586
654
  identity_str = identity.to_s
587
655
  return if identity_lifecycle_blocked?(identity_str)
588
656
 
589
657
  auth_ctx = input_frame.auth_context || {}
590
658
 
591
- # Ensure the identity is registered so reinforce has a target
592
- BondRegistry.register(identity_str, bond: nil) unless BondRegistry.bond(identity_str) != :unknown
659
+ # Duckling: first authenticated human on a node with no partner → immediate partner
660
+ if BondRegistry.bond(identity_str) == :unknown
661
+ if BondRegistry.partner_entry.nil?
662
+ BondRegistry.register(identity_str, bond: :partner, priority: :primary,
663
+ origin: :provisional, strength: 0.5)
664
+ log.info("[gaia] duckling bond formed identity=#{identity_str}")
665
+ else
666
+ BondRegistry.register(identity_str, bond: nil)
667
+ end
668
+ end
593
669
 
594
670
  # Every interaction reinforces — the evidence accumulation loop
595
671
  direct_address = input_frame.metadata[:direct_address] || false
@@ -641,7 +717,9 @@ module Legion
641
717
  return unless BondRegistry.partner_entry.nil?
642
718
 
643
719
  process_identity = resolve_process_identity
644
- prior_strength = settings&.dig(:partner, :prior_strength) || 0.5
720
+ return if process_identity.nil?
721
+
722
+ prior_strength = settings&.dig(:partner, :prior_strength) || 0.5
645
723
 
646
724
  BondRegistry.register(
647
725
  process_identity,
@@ -656,9 +734,13 @@ module Legion
656
734
  end
657
735
 
658
736
  def resolve_process_identity
659
- (defined?(Etc) && Etc.respond_to?(:loginname) && Etc.loginname) || ENV.fetch('USER', nil) || 'unknown'
737
+ return nil unless defined?(Legion::Identity::Process) && Legion::Identity::Process.respond_to?(:canonical_name)
738
+
739
+ name = Legion::Identity::Process.canonical_name
740
+ return nil if name.nil? || name == 'anonymous'
741
+
742
+ name
660
743
  end
661
- private :resolve_process_identity
662
744
 
663
745
  def record_interaction_trace(observation)
664
746
  return unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
@@ -733,25 +815,61 @@ module Legion
733
815
  synapse_ids = Array(@last_applied[:behavioral_synapse_ids])
734
816
  return if synapse_ids.empty?
735
817
 
736
- reaction_score = calibration_result[:reaction_score].to_f
737
- outcome = if reaction_score >= 0.6
738
- :success
739
- elsif reaction_score <= 0.4
740
- :failure
741
- end
818
+ outcome = calibration_outcome(calibration_result[:reaction_score].to_f)
742
819
  return unless outcome
743
820
 
821
+ identity = @last_applied[:identity]&.to_s
744
822
  multiplier = fetch_imprint_multiplier
745
823
  synapse_ids.each do |sid|
746
- BehavioralSynapse.record_outcome(id: sid, outcome: outcome, multiplier: multiplier)
824
+ grade_single_synapse(sid, outcome: outcome, identity: identity, multiplier: multiplier)
747
825
  end
826
+
748
827
  log.info("[gaia] graded synapses count=#{synapse_ids.size} outcome=#{outcome} " \
749
- "reaction_score=#{reaction_score.round(3)}")
828
+ "reaction_score=#{calibration_result[:reaction_score].to_f.round(3)}")
750
829
  @last_applied = nil
751
830
  rescue StandardError => e
752
831
  handle_exception(e, level: :warn, operation: 'gaia.grade_behavioral_synapses')
753
832
  end
754
833
 
834
+ def calibration_outcome(reaction_score)
835
+ if reaction_score >= 0.6
836
+ :success
837
+ elsif reaction_score <= 0.4
838
+ :failure
839
+ end
840
+ end
841
+
842
+ def grade_single_synapse(sid, outcome:, identity:, multiplier:)
843
+ pre_entry = BehavioralSynapse.store.values.find { |e| e[:id] == sid }
844
+ old_mode = pre_entry ? BehavioralSynapse::Math.autonomy_mode(pre_entry[:confidence].to_f) : nil
845
+
846
+ result = BehavioralSynapse.record_outcome(id: sid, outcome: outcome, multiplier: multiplier)
847
+ return unless result.is_a?(Hash) && result[:found] != false
848
+
849
+ surface_pain_frame(result, identity: identity, old_mode: old_mode)
850
+ surface_milestone_frame(result, identity: identity, old_mode: old_mode)
851
+ end
852
+
853
+ def surface_pain_frame(result, old_mode:, **)
854
+ return unless result[:status] == 'dampened' && old_mode && old_mode != :observe
855
+
856
+ msg = VisibleGrowth.pain_revert_acknowledgment(domain: result[:domain].to_s)
857
+ enqueue_growth_frame(msg) if msg
858
+ end
859
+
860
+ def surface_milestone_frame(result, identity:, old_mode:)
861
+ new_mode = BehavioralSynapse::Math.autonomy_mode(result[:confidence].to_f)
862
+ return unless old_mode && new_mode != old_mode && result[:status] == 'active'
863
+
864
+ msg = VisibleGrowth.milestone_acknowledgment(
865
+ identity: identity.to_s,
866
+ domain: result[:domain].to_s,
867
+ new_mode: new_mode,
868
+ old_mode: old_mode
869
+ )
870
+ enqueue_growth_frame(msg) if msg
871
+ end
872
+
755
873
  def run_partner_deep_learning(identity_str, observation)
756
874
  record_interaction_trace(observation)
757
875
  evaluate_calibration(observation)
@@ -1278,6 +1396,15 @@ module Legion
1278
1396
 
1279
1397
  "#{compact[0, limit]}..."
1280
1398
  end
1399
+
1400
+ # Stores a growth frame string for delivery on the next response turn.
1401
+ def enqueue_growth_frame(message)
1402
+ return if message.to_s.empty?
1403
+
1404
+ @pending_growth_frames ||= []
1405
+ @pending_growth_frames << message
1406
+ log.info("[gaia] growth frame queued: #{message.inspect}")
1407
+ end
1281
1408
  end
1282
1409
  end
1283
1410
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-gaia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.63
4
+ version: 0.9.66
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -433,6 +433,7 @@ files:
433
433
  - lib/legion/gaia/channels/teams_adapter.rb
434
434
  - lib/legion/gaia/cognitive_bus.rb
435
435
  - lib/legion/gaia/death_protocol.rb
436
+ - lib/legion/gaia/disclosure.rb
436
437
  - lib/legion/gaia/input_frame.rb
437
438
  - lib/legion/gaia/intent_classifier.rb
438
439
  - lib/legion/gaia/logging.rb
@@ -468,6 +469,7 @@ files:
468
469
  - lib/legion/gaia/tick_history.rb
469
470
  - lib/legion/gaia/tracker_persistence.rb
470
471
  - lib/legion/gaia/version.rb
472
+ - lib/legion/gaia/visible_growth.rb
471
473
  - lib/legion/gaia/workflow.rb
472
474
  - lib/legion/gaia/workflow/checkpoint.rb
473
475
  - lib/legion/gaia/workflow/definition.rb