Class: Legion::Gaia::BondStore

Inherits:
Object
  • Object
show all
Includes:
Logging::Helper
Defined in:
lib/legion/gaia/bond_store.rb

Overview

Per-identity bond evidence accumulator. Tracks a 0..1 strength score that grows through observed interactions. Supports provisional bonds (short-lived, capped at 0.5) and imprint-window acceleration.

Constant Summary collapse

DEFAULT_WEIGHTS =

Evidence weight categories — sum to 1.0

{
  direct_address: 0.20,
  interaction_frequency: 0.15,
  response_latency: 0.10,
  content_depth: 0.15,
  sentiment: 0.15,
  consistency: 0.10,
  helpfulness: 0.10,
  shared_vulnerability: 0.05
}.freeze
DEFAULT_BELL_THRESHOLDS =

Bell thresholds — score crosses upward boundary

[0.25, 0.50, 0.75, 0.90, 1.0].freeze
MAX_EVIDENCE =

Max evidence entries retained per identity (ring buffer)

200
DEFAULT_PROVISIONAL_TTL =

Default provisional TTL — 24 hours

24 * 3600
DECAY_FLOOR =

Maximum decay factor (floor) — score doesn't drop below 70% of raw

0.7
PROVISIONAL_CEILING =

Provisional strength ceiling until confirmed

0.5

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(identity:, bond_role:, started_at: Time.now.utc, weights: DEFAULT_WEIGHTS, bell_thresholds: DEFAULT_BELL_THRESHOLDS, provisional_ttl: DEFAULT_PROVISIONAL_TTL) ⇒ BondStore

Returns a new instance of BondStore.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/legion/gaia/bond_store.rb', line 43

def initialize(identity:, bond_role:, started_at: Time.now.utc, weights: DEFAULT_WEIGHTS,
               bell_thresholds: DEFAULT_BELL_THRESHOLDS,
               provisional_ttl: DEFAULT_PROVISIONAL_TTL)
  @identity = identity.to_s
  @bond_role = (bond_role || :unknown).to_sym
  @started_at = started_at
  @weights = weights
  @bell_thresholds = bell_thresholds
  @provisional_ttl = provisional_ttl

  @raw_score = 0.0
  @evidence_log = []
  @dirty = false
  @last_evidence_at = nil
  @provisional_expires_at = set_provisional_expiry!
  @crossed_bells = []
  @identity_principal_id = nil
  @identity_canonical_name = nil
  @identity_id = nil
end

Instance Attribute Details

#bond_roleObject (readonly)

Returns the value of attribute bond_role.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def bond_role
  @bond_role
end

#evidence_logObject (readonly)

Returns the value of attribute evidence_log.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def evidence_log
  @evidence_log
end

#identityObject (readonly)

Returns the value of attribute identity.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def identity
  @identity
end

#identity_canonical_nameObject (readonly)

Returns the value of attribute identity_canonical_name.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def identity_canonical_name
  @identity_canonical_name
end

#identity_idObject (readonly)

Returns the value of attribute identity_id.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def identity_id
  @identity_id
end

#identity_principal_idObject (readonly)

Returns the value of attribute identity_principal_id.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def identity_principal_id
  @identity_principal_id
end

#provisional_expires_atObject (readonly)

Returns the value of attribute provisional_expires_at.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def provisional_expires_at
  @provisional_expires_at
end

#provisional_ttlObject (readonly)

Returns the value of attribute provisional_ttl.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def provisional_ttl
  @provisional_ttl
end

#raw_scoreObject (readonly)

Returns the value of attribute raw_score.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def raw_score
  @raw_score
end

#started_atObject (readonly)

Returns the value of attribute started_at.



40
41
42
# File 'lib/legion/gaia/bond_store.rb', line 40

def started_at
  @started_at
end

Instance Method Details

#accumulate(observation, imprint_multiplier: 1.0) ⇒ Object

Accumulate evidence from an observation hash. Returns the delta added to raw_score.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/legion/gaia/bond_store.rb', line 66

def accumulate(observation, imprint_multiplier: 1.0)
  base_delta = extract_evidence_delta(observation)
  imprint = imprint_multiplier || 1.0
  delta = base_delta * imprint * diminishing_factor

  # Apply decay discount to incoming evidence (older = less impactful)
  return 0.0 if delta.zero?

  @raw_score = [@raw_score + delta, 1.0].min
  @last_evidence_at = Time.now.utc

  # Capture identity columns from observation
  @identity_principal_id ||= observation[:identity_principal_id]
  @identity_canonical_name ||= observation[:identity_canonical_name]
  @identity_id ||= observation[:identity_id]

  # Log evidence (ring buffer)
  log_entry = {
    timestamp: observation[:timestamp] || Time.now.utc,
    delta: delta.round(6),
    base_delta: base_delta.round(6),
    direct_address: observation[:direct_address] || false,
    content_length: observation[:content_length] || 0,
    channel: (observation[:channel] || :unknown).to_s
  }
  @evidence_log << log_entry
  @evidence_log.shift if @evidence_log.size > MAX_EVIDENCE

  @dirty = true
  log.debug("[bond] accumulate identity=#{@identity} delta=#{delta.round(6)} " \
            "score=#{@raw_score.round(4)} multiplier=#{imprint}")
  delta
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'gaia.bond_store.accumulate', identity: @identity)
  0.0
end

#bell?(new_strength) ⇒ Boolean

Check if a new strength value crosses any uncrossed bell threshold. Returns list of newly crossed thresholds.

Returns:

  • (Boolean)


123
124
125
126
127
128
129
# File 'lib/legion/gaia/bond_store.rb', line 123

def bell?(new_strength)
  new_bells = @bell_thresholds.select do |threshold|
    new_strength >= threshold && !@crossed_bells.include?(threshold)
  end
  @crossed_bells += new_bells if new_bells.any?
  new_bells
end

#confirm!Object

Mark this provisional bond as confirmed (remove cap and expiry).



132
133
134
135
136
137
138
139
140
# File 'lib/legion/gaia/bond_store.rb', line 132

def confirm!
  return unless @bond_role == :provisional

  old_role = @bond_role
  @bond_role = :partner
  @provisional_expires_at = nil
  @dirty = true
  log.info("[bond] confirmed provisional identity=#{@identity} role=#{old_role}->#{@bond_role}")
end

#dirty?Boolean

Returns:

  • (Boolean)


146
147
148
# File 'lib/legion/gaia/bond_store.rb', line 146

def dirty?
  @dirty
end

#from_apollo(store:) ⇒ Object

Reconstruct state from Apollo query results (hydration).



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/legion/gaia/bond_store.rb', line 176

def from_apollo(store:)
  result = store.query(text: "bond #{@identity}", tags: %w[bond gaia bond_evidence])
  return unless result.is_a?(Hash) && result[:success] && result[:results]

  result[:results].each do |entry|
    # Extract strength from confidence field
    raw_confidence = entry[:confidence] || 0.0
    @raw_score = [raw_confidence, 1.0].min if raw_confidence.is_a?(Numeric)

    # Extract identity columns
    @identity_principal_id ||= entry[:identity_principal_id]
    @identity_canonical_name ||= entry[:identity_canonical_name]
    @identity_id ||= entry[:identity_id]
  end
  @last_evidence_at = Time.now.utc
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'gaia.bond_store.from_apollo', identity: @identity)
end

#mark_clean!Object



150
151
152
# File 'lib/legion/gaia/bond_store.rb', line 150

def mark_clean!
  @dirty = false
end

#provisional?Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/legion/gaia/bond_store.rb', line 142

def provisional?
  @bond_role == :provisional
end

#strengthObject

Returns 0..1 strength, applying time decay and provisional cap.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/legion/gaia/bond_store.rb', line 104

def strength
  decayed = apply_decay(@raw_score)

  if @bond_role == :provisional && @provisional_expires_at
    return 0.0 if Time.now.utc > @provisional_expires_at

    # Expired provisional transitions to unknown
    if Time.now.utc > @provisional_expires_at
      @bond_role = :unknown
      log.debug("[bond] provisional expired identity=#{@identity}")
    end
    return [decayed, PROVISIONAL_CEILING].min
  end

  decayed
end

#to_apollo_entriesObject

Convert to Apollo upsert entries for persistence.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/legion/gaia/bond_store.rb', line 155

def to_apollo_entries
  entries = []

  # Summary entry — current strength snapshot
  entries << {
    content: content_from_values,
    tags: %w[bond gaia bond_evidence],
    confidence: strength,
    access_scope: 'local',
    identity_canonical_name: @identity_canonical_name,
    identity_principal_id: @identity_principal_id,
    identity_id: @identity_id
  }

  # Evidence batch entries (last N entries since clean)
  entries += evidence_entries

  entries
end