99 "sync"
1010 "time"
1111
12+ "github.com/BackendStack21/odek/internal/memory/extended"
1213 "github.com/BackendStack21/odek/internal/session"
1314)
1415
@@ -102,6 +103,12 @@ type MemoryConfig struct {
102103 // See EmbeddingConfig for the "http" provider that enables real
103104 // semantic similarity via any OpenAI-compatible embeddings API.
104105 Embedding * EmbeddingConfig `json:"embedding,omitempty"`
106+
107+ // Extended configures the Extended Memory subsystem (atomic facts/
108+ // observations extracted from user messages and recalled via semantic
109+ // search). nil means "use subsystem defaults"; the subsystem is opt-in
110+ // and invisible when Extended.Enabled is false.
111+ Extended * extended.Config `json:"extended,omitempty"`
105112}
106113
107114// BoolPtr returns a pointer to a bool value.
@@ -131,6 +138,7 @@ func DefaultMemoryConfig() MemoryConfig {
131138 EpisodeDedupThreshold : defaultEpisodeDedupThreshold ,
132139 MaxEpisodes : defaultMaxEpisodes ,
133140 EpisodeTTLDays : 0 , // TTL disabled by default
141+ Extended : nil ,
134142 }
135143}
136144
@@ -150,6 +158,13 @@ type MemoryManager struct {
150158 merge * MergeDetector
151159 llm LLMClient
152160 cfg MemoryConfig
161+ extended * extended.ExtendedMemory
162+
163+ // extendedContext caches the current session context for user-message
164+ // extraction callbacks that arrive without explicit provenance.
165+ extSessionID string
166+ extProject string
167+ extTurn int
153168
154169 // notifier receives memory lifecycle events (facts + episodes). Defaults to
155170 // a NoopMemoryNotifier so the fire path is always safe without a nil check.
@@ -230,6 +245,9 @@ func NewMemoryManager(memoryDir string, llc LLMClient, cfg MemoryConfig) *Memory
230245 if cfg .Embedding != nil {
231246 def .Embedding = cfg .Embedding
232247 }
248+ if cfg .Extended != nil {
249+ def .Extended = cfg .Extended
250+ }
233251 cfg = def
234252
235253 factsDir := memoryDir
@@ -281,6 +299,76 @@ func (m *MemoryManager) SetNotifier(n MemoryNotifier) {
281299 }
282300}
283301
302+ // InitExtended creates the Extended Memory subsystem using the provided
303+ // dedicated memory LLM client. It is safe to call multiple times; subsequent
304+ // calls are ignored. Callers should invoke this after NewMemoryManager once
305+ // the memory LLM has been resolved.
306+ func (m * MemoryManager ) InitExtended (memoryLLM extended.LLMClient , memoryDir string ) {
307+ if m .extended != nil {
308+ return
309+ }
310+ if m .cfg .Extended == nil {
311+ m .cfg .Extended = & extended.Config {}
312+ }
313+ cfg := extended .Resolve (* m .cfg .Extended )
314+ if cfg .Embedding == nil && m .cfg .Embedding != nil {
315+ cfg .Embedding = m .cfg .Embedding
316+ }
317+ if memoryDir == "" {
318+ memoryDir = m .facts .dir
319+ }
320+ extDir := filepath .Join (memoryDir , "extended" )
321+ m .extended = extended .New (extDir , memoryLLM , cfg )
322+ }
323+
324+ // OnUserMessage routes a user message to Extended Memory for atom extraction.
325+ func (m * MemoryManager ) OnUserMessage (ctx extended.AtomContext , msg string ) {
326+ if m .extended == nil {
327+ return
328+ }
329+ m .extended .OnUserMessage (ctx , msg )
330+ }
331+
332+ // FormatExtendedContext returns ranked Extended Memory context for the query.
333+ func (m * MemoryManager ) FormatExtendedContext (query string ) string {
334+ if m .extended == nil {
335+ return ""
336+ }
337+ return m .extended .FormatExtendedContext (query )
338+ }
339+
340+ // SetSessionContext propagates session/project identifiers to all memory tiers
341+ // that need them (currently Extended Memory).
342+ func (m * MemoryManager ) SetSessionContext (sessionID , project string ) {
343+ m .extSessionID = sessionID
344+ m .extProject = project
345+ if m .extended != nil {
346+ m .extended .SetSessionContext (sessionID , project )
347+ }
348+ }
349+
350+ // OnUserMessageLoop routes a user message to Extended Memory using the
351+ // session context previously set via SetSessionContext. It is the callback
352+ // used by the agent loop to trigger atom extraction when a new user message
353+ // arrives.
354+ func (m * MemoryManager ) OnUserMessageLoop (msg string ) {
355+ if m .extended == nil {
356+ return
357+ }
358+ m .extTurn ++
359+ ctx := extended.AtomContext {
360+ SessionID : m .extSessionID ,
361+ Project : m .extProject ,
362+ Turn : m .extTurn ,
363+ }
364+ m .extended .OnUserMessage (ctx , msg )
365+ }
366+
367+ // Extended returns the Extended Memory subsystem, or nil if not initialized.
368+ func (m * MemoryManager ) Extended () * extended.ExtendedMemory {
369+ return m .extended
370+ }
371+
284372// notify fires an event on the configured notifier, stamping the UTC timestamp
285373// when the caller left it zero. Safe even before SetNotifier (nil → no-op).
286374func (m * MemoryManager ) notify (ev MemoryEvent ) {
0 commit comments