File size: 6,373 Bytes
7d3b88b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
 * memoryManager.js β€” Profile (localStorage) + Session (sessionStorage) handler.
 *
 * Profile: Stable user facts (name, job, preferences). Max 15 entries.
 *   - Confidence-based conflict resolution (no blind overwrite)
 *   - Identity keys (name, job, role) are never deleted, only updated with higher confidence
 *   - Dedup by key
 *
 * Session: Temporary context (references, tasks). Max 10 entries, FIFO.
 *   - Only 'reference' and 'task' intents stored
 *   - Only last 5 injected into requests
 */

const PROFILE_STORAGE_KEY = 'agent_profile_memory'
const SESSION_STORAGE_KEY = 'agent_session_memory'

const MAX_PROFILE_ENTRIES = 15
const MAX_SESSION_ENTRIES = 10
const MAX_SESSION_INJECTION = 5

const IDENTITY_KEYS = new Set(['name', 'job', 'role', 'profession', 'location'])
const CONFIDENCE_TOLERANCE = 0.1 // within this range = "uncertain", keep both

// ── Profile Memory (localStorage) ─────────────────────────────────────

function getProfile() {
  try {
    const raw = localStorage.getItem(PROFILE_STORAGE_KEY)
    return raw ? JSON.parse(raw) : []
  } catch {
    return []
  }
}

function setProfile(entries) {
  try {
    localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(entries))
  } catch (e) {
    console.warn('[MemoryManager] Failed to write profile:', e)
  }
}

/**
 * Save a profile entry with conflict resolution.
 *
 * Rules:
 * - New confidence > existing β†’ overwrite
 * - New confidence β‰ˆ existing (within tolerance) and values differ β†’ keep both (as array)
 * - New confidence < existing β†’ discard new
 * - Identity keys are never deleted, only updated with higher confidence
 * - Max 15 entries; on overflow, evict lowest-confidence non-identity entry
 */
function saveProfile(extraction) {
  const { data, confidence, intent } = extraction
  const key = data?.key
  const value = data?.value

  if (!key || !value) return

  const entries = getProfile()
  const existingIndex = entries.findIndex((e) => e.key === key)

  if (existingIndex >= 0) {
    const existing = entries[existingIndex]
    const confDiff = confidence - existing.confidence

    if (confDiff > CONFIDENCE_TOLERANCE) {
      // New is clearly higher confidence β†’ overwrite
      entries[existingIndex] = { key, value, confidence, intent, timestamp: Date.now() }
    } else if (confDiff >= -CONFIDENCE_TOLERANCE && existing.value !== value) {
      // Similar confidence but different values β†’ keep both
      const combinedValue = Array.isArray(existing.value)
        ? [...new Set([...existing.value, value])]
        : existing.value === value
          ? existing.value
          : [existing.value, value]
      entries[existingIndex] = {
        key,
        value: combinedValue,
        confidence: Math.max(confidence, existing.confidence),
        intent,
        timestamp: Date.now(),
      }
    }
    // else: new confidence is lower β†’ discard (do nothing)
  } else {
    // New key β€” add it
    entries.push({ key, value, confidence, intent, timestamp: Date.now() })
  }

  // Enforce cap
  while (entries.length > MAX_PROFILE_ENTRIES) {
    // Find lowest-confidence non-identity entry to evict
    let evictIndex = -1
    let lowestConf = Infinity

    for (let i = 0; i < entries.length; i++) {
      if (!IDENTITY_KEYS.has(entries[i].key) && entries[i].confidence < lowestConf) {
        lowestConf = entries[i].confidence
        evictIndex = i
      }
    }

    if (evictIndex >= 0) {
      entries.splice(evictIndex, 1)
    } else {
      // All entries are identity β€” evict oldest
      entries.shift()
    }
  }

  setProfile(entries)
}

// ── Session Memory (sessionStorage) ───────────────────────────────────

function getSession() {
  try {
    const raw = sessionStorage.getItem(SESSION_STORAGE_KEY)
    return raw ? JSON.parse(raw) : []
  } catch {
    return []
  }
}

function setSession(entries) {
  try {
    sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(entries))
  } catch (e) {
    console.warn('[MemoryManager] Failed to write session:', e)
  }
}

/**
 * Save a session entry. Only 'reference' and 'task' intents accepted.
 * FIFO eviction at max 10 entries.
 */
function saveSession(extraction) {
  const { data, confidence, intent } = extraction
  const key = data?.key
  const value = data?.value

  if (!key || !value) return

  // Only store reference and task intents
  if (intent !== 'reference' && intent !== 'task') return

  const entries = getSession()

  // Dedup by key β€” update if exists
  const existingIndex = entries.findIndex((e) => e.key === key)
  if (existingIndex >= 0) {
    entries[existingIndex] = { key, value, confidence, intent, timestamp: Date.now() }
  } else {
    entries.push({ key, value, confidence, intent, timestamp: Date.now() })
  }

  // FIFO eviction
  while (entries.length > MAX_SESSION_ENTRIES) {
    entries.shift()
  }

  setSession(entries)
}

// ── Injection helpers ─────────────────────────────────────────────────

/**
 * Get formatted profile entries for backend injection.
 * Returns all profile entries.
 */
function getProfileForInjection() {
  return getProfile()
}

/**
 * Get formatted session entries for backend injection.
 * Returns only the last 5 entries (most recent).
 */
function getSessionForInjection() {
  const entries = getSession()
  return entries.slice(-MAX_SESSION_INJECTION)
}

// ── Main handler ──────────────────────────────────────────────────────

/**
 * Process a memory extraction result from the backend.
 * Routes to profile or session storage based on memory_type.
 */
function processExtraction(extraction) {
  if (!extraction || !extraction.store) return

  if (extraction.memory_type === 'profile') {
    saveProfile(extraction)
  } else if (extraction.memory_type === 'session') {
    saveSession(extraction)
  }
}

export const MemoryManager = {
  getProfileForInjection,
  getSessionForInjection,
  processExtraction,
  // Exposed for debugging / insights panel
  getProfile,
  getSession,
}

export default MemoryManager