Skip to content

Commit aca26f7

Browse files
authored
Merge pull request #335 from AppSprout-dev/feat/mind-graph-page
feat: Mind graph page, live cognitive metrics, and system analysis
2 parents 470e207 + 442e999 commit aca26f7

10 files changed

Lines changed: 1647 additions & 276 deletions

File tree

internal/agent/consolidation/agent.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,17 @@ func (ca *ConsolidationAgent) runCycle(ctx context.Context) (*CycleReport, error
383383
// Publish consolidation completed event
384384
if ca.bus != nil {
385385
_ = ca.bus.Publish(ctx, events.ConsolidationCompleted{
386-
DurationMs: report.Duration.Milliseconds(),
387-
MemoriesProcessed: report.MemoriesProcessed,
388-
MemoriesDecayed: report.MemoriesDecayed,
389-
MergedClusters: report.MergesPerformed,
390-
AssociationsPruned: report.AssociationsPruned,
391-
Ts: time.Now(),
386+
DurationMs: report.Duration.Milliseconds(),
387+
MemoriesProcessed: report.MemoriesProcessed,
388+
MemoriesDecayed: report.MemoriesDecayed,
389+
MergedClusters: report.MergesPerformed,
390+
AssociationsPruned: report.AssociationsPruned,
391+
TransitionedFading: report.TransitionedFading,
392+
TransitionedArchived: report.TransitionedArchived,
393+
PatternsExtracted: report.PatternsExtracted,
394+
PatternsDecayed: report.PatternsDecayed,
395+
NeverRecalledArchived: report.NeverRecalledArchived,
396+
Ts: time.Now(),
392397
})
393398
}
394399

internal/api/routes/backfill.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package routes
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net/http"
7+
"time"
8+
9+
"github.com/appsprout-dev/mnemonic/internal/llm"
10+
"github.com/appsprout-dev/mnemonic/internal/store"
11+
)
12+
13+
// BackfillResponse reports what the backfill operation did.
14+
type BackfillResponse struct {
15+
Total int `json:"total"`
16+
Embedded int `json:"embedded"`
17+
Failed int `json:"failed"`
18+
Skipped int `json:"skipped"`
19+
Errors []string `json:"errors,omitempty"`
20+
}
21+
22+
// HandleBackfillEmbeddings finds memories with empty embeddings and generates them.
23+
func HandleBackfillEmbeddings(s store.Store, provider llm.Provider, log *slog.Logger) http.HandlerFunc {
24+
return func(w http.ResponseWriter, r *http.Request) {
25+
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
26+
defer cancel()
27+
28+
// Find all active memories missing embeddings
29+
memories, err := s.ListMemories(ctx, "", 500, 0)
30+
if err != nil {
31+
log.Error("backfill: failed to list memories", "error", err)
32+
writeError(w, http.StatusInternalServerError, "failed to list memories", "STORE_ERROR")
33+
return
34+
}
35+
36+
var missing []store.Memory
37+
for _, m := range memories {
38+
if len(m.Embedding) == 0 {
39+
missing = append(missing, m)
40+
}
41+
}
42+
43+
if len(missing) == 0 {
44+
writeJSON(w, http.StatusOK, BackfillResponse{Total: 0})
45+
return
46+
}
47+
48+
log.Info("backfill: starting embedding backfill", "missing", len(missing))
49+
50+
// Quick sanity check: can we embed at all?
51+
testEmb, testErr := provider.Embed(ctx, "test embedding sanity check")
52+
if testErr != nil {
53+
log.Error("backfill: embedding sanity check failed", "error", testErr)
54+
writeJSON(w, http.StatusOK, BackfillResponse{Total: len(missing), Errors: []string{"sanity check failed: " + testErr.Error()}})
55+
return
56+
}
57+
log.Info("backfill: sanity check passed", "dims", len(testEmb))
58+
59+
resp := BackfillResponse{Total: len(missing)}
60+
61+
for _, mem := range missing {
62+
select {
63+
case <-ctx.Done():
64+
log.Warn("backfill: context cancelled", "embedded", resp.Embedded, "remaining", resp.Total-resp.Embedded-resp.Failed)
65+
writeJSON(w, http.StatusOK, resp)
66+
return
67+
default:
68+
}
69+
70+
// Build embedding text from summary + content (same as encoding agent)
71+
text := mem.Summary + " " + mem.Content
72+
if len(text) > 4000 {
73+
text = text[:4000]
74+
}
75+
76+
embedding, err := provider.Embed(ctx, text)
77+
if err != nil {
78+
resp.Errors = append(resp.Errors, "embed:"+mem.ID[:8]+":"+err.Error())
79+
resp.Failed++
80+
continue
81+
}
82+
83+
if len(embedding) == 0 {
84+
resp.Skipped++
85+
continue
86+
}
87+
88+
// Use targeted update to avoid FK issues with raw_id
89+
if err := s.UpdateEmbedding(ctx, mem.ID, embedding); err != nil {
90+
resp.Errors = append(resp.Errors, "update:"+mem.ID[:8]+":"+err.Error())
91+
resp.Failed++
92+
continue
93+
}
94+
95+
resp.Embedded++
96+
log.Debug("backfill: embedded memory", "id", mem.ID, "dims", len(embedding))
97+
}
98+
99+
log.Info("backfill: completed", "total", resp.Total, "embedded", resp.Embedded, "failed", resp.Failed)
100+
writeJSON(w, http.StatusOK, resp)
101+
}
102+
}

internal/api/routes/retrieval.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package routes
2+
3+
import (
4+
"log/slog"
5+
"net/http"
6+
7+
"github.com/appsprout-dev/mnemonic/internal/agent/retrieval"
8+
)
9+
10+
// HandleRetrievalStats returns the retrieval agent's in-memory performance stats.
11+
func HandleRetrievalStats(retriever *retrieval.RetrievalAgent, log *slog.Logger) http.HandlerFunc {
12+
return func(w http.ResponseWriter, r *http.Request) {
13+
if retriever == nil {
14+
writeJSON(w, http.StatusOK, map[string]any{
15+
"total_queries": 0,
16+
"total_memories_retrieved": 0,
17+
"avg_memories_per_query": 0,
18+
"avg_synthesis_ms": 0,
19+
})
20+
return
21+
}
22+
writeJSON(w, http.StatusOK, retriever.GetStats())
23+
}
24+
}

internal/api/routes/ws.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ func HandleWebSocket(bus events.Bus, log *slog.Logger) http.HandlerFunc {
100100
events.TypeSystemHealth,
101101
events.TypeWatcherEvent,
102102
events.TypeEpisodeClosed,
103+
events.TypePatternDiscovered,
104+
events.TypeAbstractionCreated,
105+
events.TypeMemoryAmended,
106+
events.TypeSessionEnded,
103107
}
104108

105109
for _, eventType := range eventTypes {
@@ -208,6 +212,16 @@ func wsConnEventToMessage(evt events.Event) WebSocketMessage {
208212
payload = e
209213
case events.WatcherEvent:
210214
payload = e
215+
case events.EpisodeClosed:
216+
payload = e
217+
case events.PatternDiscovered:
218+
payload = e
219+
case events.AbstractionCreated:
220+
payload = e
221+
case events.MemoryAmended:
222+
payload = e
223+
case events.SessionEnded:
224+
payload = e
211225
default:
212226
// Fallback for unknown event types
213227
payload = map[string]interface{}{}

internal/api/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ func (s *Server) registerRoutes() {
106106
// Activity (watcher-derived concept tracker for MCP sync)
107107
s.mux.HandleFunc("GET /api/v1/activity", routes.HandleActivity(s.deps.Retriever, s.deps.Log))
108108

109+
// Retrieval stats
110+
s.mux.HandleFunc("GET /api/v1/retrieval/stats", routes.HandleRetrievalStats(s.deps.Retriever, s.deps.Log))
111+
112+
// Embedding backfill
113+
s.mux.HandleFunc("POST /api/v1/embeddings/backfill", routes.HandleBackfillEmbeddings(s.deps.Store, s.deps.LLM, s.deps.Log))
114+
109115
// Feedback
110116
s.mux.HandleFunc("POST /api/v1/feedback", routes.HandleFeedback(s.deps.Store, s.deps.Log))
111117

internal/events/types.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,17 @@ func (e ConsolidationStarted) EventTimestamp() time.Time { return e.Ts }
6262

6363
// ConsolidationCompleted is emitted when a consolidation cycle finishes.
6464
type ConsolidationCompleted struct {
65-
DurationMs int64 `json:"duration_ms"`
66-
MemoriesProcessed int `json:"memories_processed"`
67-
MemoriesDecayed int `json:"memories_decayed"`
68-
MergedClusters int `json:"merged_clusters"`
69-
AssociationsPruned int `json:"associations_pruned"`
70-
Ts time.Time `json:"timestamp"`
65+
DurationMs int64 `json:"duration_ms"`
66+
MemoriesProcessed int `json:"memories_processed"`
67+
MemoriesDecayed int `json:"memories_decayed"`
68+
MergedClusters int `json:"merged_clusters"`
69+
AssociationsPruned int `json:"associations_pruned"`
70+
TransitionedFading int `json:"transitioned_fading"`
71+
TransitionedArchived int `json:"transitioned_archived"`
72+
PatternsExtracted int `json:"patterns_extracted"`
73+
PatternsDecayed int `json:"patterns_decayed"`
74+
NeverRecalledArchived int `json:"never_recalled_archived"`
75+
Ts time.Time `json:"timestamp"`
7176
}
7277

7378
func (e ConsolidationCompleted) EventType() string { return TypeConsolidationCompleted }

internal/store/sqlite/sqlite.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,28 @@ func (s *SQLiteStore) UpdateMemory(ctx context.Context, mem store.Memory) error
930930
}
931931

932932
// UpdateSalience updates the salience of a memory.
933+
func (s *SQLiteStore) UpdateEmbedding(ctx context.Context, id string, embedding []float32) error {
934+
var embeddingBlob []byte
935+
if len(embedding) > 0 {
936+
embeddingBlob = encodeEmbedding(embedding)
937+
}
938+
939+
query := `UPDATE memories SET embedding = ?, updated_at = ? WHERE id = ?`
940+
result, err := s.db.ExecContext(ctx, query, embeddingBlob, time.Now().Format(time.RFC3339), id)
941+
if err != nil {
942+
return fmt.Errorf("failed to update embedding: %w", err)
943+
}
944+
945+
rowsAffected, err := result.RowsAffected()
946+
if err != nil {
947+
return fmt.Errorf("failed to get rows affected: %w", err)
948+
}
949+
if rowsAffected == 0 {
950+
return fmt.Errorf("memory with id %s: %w", id, store.ErrNotFound)
951+
}
952+
return nil
953+
}
954+
933955
func (s *SQLiteStore) UpdateSalience(ctx context.Context, id string, salience float32) error {
934956
query := `UPDATE memories SET salience = ?, updated_at = ? WHERE id = ?`
935957

internal/store/store.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ type Store interface {
378378
GetMemoryByRawID(ctx context.Context, rawID string) (Memory, error)
379379
UpdateMemory(ctx context.Context, mem Memory) error
380380
UpdateSalience(ctx context.Context, id string, salience float32) error
381+
UpdateEmbedding(ctx context.Context, id string, embedding []float32) error
381382
UpdateState(ctx context.Context, id string, state string) error
382383
IncrementAccess(ctx context.Context, id string) error
383384
ListMemories(ctx context.Context, state string, limit, offset int) ([]Memory, error)

internal/store/storetest/mock.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ func (MockStore) GetMemoryByRawID(context.Context, string) (store.Memory, error)
4444
return store.Memory{}, nil
4545
}
4646
func (MockStore) UpdateMemory(context.Context, store.Memory) error { return nil }
47-
func (MockStore) UpdateSalience(context.Context, string, float32) error { return nil }
47+
func (MockStore) UpdateSalience(context.Context, string, float32) error { return nil }
48+
func (MockStore) UpdateEmbedding(context.Context, string, []float32) error { return nil }
4849
func (MockStore) UpdateState(context.Context, string, string) error { return nil }
4950
func (MockStore) IncrementAccess(context.Context, string) error { return nil }
5051
func (MockStore) ListMemories(context.Context, string, int, int) ([]store.Memory, error) {

0 commit comments

Comments
 (0)