Changeset 0.30.1 (#200)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -5,13 +5,15 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/memory"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// MemoryHandler provides REST endpoints for memory management.
|
||||
type MemoryHandler struct {
|
||||
stores store.Stores
|
||||
stores store.Stores
|
||||
compactor *memory.Compactor
|
||||
}
|
||||
|
||||
// NewMemoryHandler creates a new memory handler.
|
||||
@@ -19,6 +21,11 @@ func NewMemoryHandler(stores store.Stores) *MemoryHandler {
|
||||
return &MemoryHandler{stores: stores}
|
||||
}
|
||||
|
||||
// SetCompactor wires the compactor for the /compact endpoint.
|
||||
func (h *MemoryHandler) SetCompactor(c *memory.Compactor) {
|
||||
h.compactor = c
|
||||
}
|
||||
|
||||
// ListMyMemories returns the current user's memories with optional filters.
|
||||
// GET /api/v1/memories?status=active&query=...
|
||||
func (h *MemoryHandler) ListMyMemories(c *gin.Context) {
|
||||
@@ -254,3 +261,37 @@ func (h *MemoryHandler) BulkApprove(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"approved": approved})
|
||||
}
|
||||
|
||||
// CompactMemories triggers memory compaction for the current user.
|
||||
// POST /api/v1/memories/compact
|
||||
func (h *MemoryHandler) CompactMemories(c *gin.Context) {
|
||||
if h.compactor == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "compaction not available"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.GetString("team_id")
|
||||
var tID *string
|
||||
if teamID != "" {
|
||||
tID = &teamID
|
||||
}
|
||||
|
||||
result, err := h.compactor.Compact(c.Request.Context(), userID, tID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"before": result.Before,
|
||||
"after": result.After,
|
||||
"merged": result.Merged,
|
||||
"archived": result.Archived,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ func main() {
|
||||
// v0.27.3: task_create tool (AI spawns sub-tasks)
|
||||
tools.RegisterTaskTools(stores)
|
||||
memExtractor := memory.NewExtractor(stores, roleResolver, kbEmbedder)
|
||||
memCompactor := memory.NewCompactor(stores, roleResolver)
|
||||
memScanner := memory.NewScanner(memExtractor, stores, memory.ScannerConfig{})
|
||||
memScanner.Start()
|
||||
defer memScanner.Stop()
|
||||
@@ -909,12 +910,14 @@ func main() {
|
||||
|
||||
// Memory management (v0.18.0)
|
||||
memH := handlers.NewMemoryHandler(stores)
|
||||
memH.SetCompactor(memCompactor)
|
||||
protected.GET("/memories", memH.ListMyMemories)
|
||||
protected.PUT("/memories/:id", memH.UpdateMemory)
|
||||
protected.DELETE("/memories/:id", memH.DeleteMemory)
|
||||
protected.POST("/memories/:id/approve", memH.ApproveMemory)
|
||||
protected.POST("/memories/:id/reject", memH.RejectMemory)
|
||||
protected.GET("/memories/count", memH.MemoryCount)
|
||||
protected.POST("/memories/compact", memH.CompactMemories)
|
||||
|
||||
// Teams (user: my teams)
|
||||
teams := handlers.NewTeamHandler(stores, keyResolver)
|
||||
|
||||
157
server/memory/compactor.go
Normal file
157
server/memory/compactor.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/roles"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// compactionThreshold is the minimum number of active memories before
|
||||
// compaction is considered worthwhile for a user.
|
||||
const compactionThreshold = 30
|
||||
|
||||
// compactionPrompt instructs the utility model to merge related memories.
|
||||
const compactionPrompt = `You are a memory compaction assistant. Given a set of user memories (key-value facts), merge related or redundant entries into fewer, more precise facts.
|
||||
|
||||
Rules:
|
||||
- Combine facts about the same topic into a single entry
|
||||
- Remove duplicates, keeping the most specific/recent version
|
||||
- Preserve all unique information — do not drop facts
|
||||
- Keep keys short (2-5 words, lowercase)
|
||||
- Keep values concise (1-2 sentences max)
|
||||
- Set confidence based on how well-supported the merged fact is
|
||||
|
||||
Respond ONLY with a JSON array of merged facts:
|
||||
[{"key": "...", "value": "...", "confidence": 0.0-1.0}]
|
||||
|
||||
If no merging is possible, return the original facts unchanged.`
|
||||
|
||||
// Compactor merges related memories to reduce count and improve quality.
|
||||
type Compactor struct {
|
||||
stores store.Stores
|
||||
roleResolver *roles.Resolver
|
||||
}
|
||||
|
||||
// NewCompactor creates a memory compaction service.
|
||||
func NewCompactor(stores store.Stores, rr *roles.Resolver) *Compactor {
|
||||
return &Compactor{stores: stores, roleResolver: rr}
|
||||
}
|
||||
|
||||
// CompactResult holds the outcome of a compaction run.
|
||||
type CompactResult struct {
|
||||
Before int `json:"before"`
|
||||
After int `json:"after"`
|
||||
Merged int `json:"merged"`
|
||||
Archived int `json:"archived"`
|
||||
}
|
||||
|
||||
// Compact loads active memories for a user, sends them to the utility model
|
||||
// for merging, then upserts merged entries and archives originals.
|
||||
func (c *Compactor) Compact(ctx context.Context, userID string, teamID *string) (*CompactResult, error) {
|
||||
if c.stores.Memories == nil {
|
||||
return nil, fmt.Errorf("memory store not available")
|
||||
}
|
||||
|
||||
// Load all active user-scope memories
|
||||
memories, err := c.stores.Memories.List(ctx, models.MemoryFilter{
|
||||
Scope: models.MemoryScopeUser,
|
||||
OwnerID: userID,
|
||||
Status: models.MemoryStatusActive,
|
||||
Limit: 500,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list memories: %w", err)
|
||||
}
|
||||
|
||||
if len(memories) < compactionThreshold {
|
||||
return &CompactResult{Before: len(memories), After: len(memories)}, nil
|
||||
}
|
||||
|
||||
// Build memory text for the model
|
||||
var sb strings.Builder
|
||||
for i, m := range memories {
|
||||
sb.WriteString(fmt.Sprintf("%d. [%s] %s (confidence: %.2f)\n", i+1, m.Key, m.Value, m.Confidence))
|
||||
}
|
||||
|
||||
apiMessages := []providers.Message{
|
||||
{Role: "system", Content: compactionPrompt},
|
||||
{Role: "user", Content: fmt.Sprintf("Compact these %d memories:\n\n%s", len(memories), sb.String())},
|
||||
}
|
||||
|
||||
result, err := c.roleResolver.Complete(ctx, "utility", userID, teamID, apiMessages)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compaction completion: %w", err)
|
||||
}
|
||||
|
||||
// Parse merged facts
|
||||
merged, err := parseExtractionResponse(result.Content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse compaction response: %w", err)
|
||||
}
|
||||
|
||||
if len(merged) == 0 || len(merged) >= len(memories) {
|
||||
// Model returned nothing useful or didn't reduce — skip
|
||||
log.Printf("🧠 memory compaction: user %s — model returned %d facts (was %d), skipping",
|
||||
userID, len(merged), len(memories))
|
||||
return &CompactResult{Before: len(memories), After: len(memories)}, nil
|
||||
}
|
||||
|
||||
// Archive all original memories
|
||||
archived := 0
|
||||
for _, m := range memories {
|
||||
if err := c.stores.Memories.Archive(ctx, m.ID); err == nil {
|
||||
archived++
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert merged memories
|
||||
for _, fact := range merged {
|
||||
mem := &models.Memory{
|
||||
Scope: models.MemoryScopeUser,
|
||||
OwnerID: userID,
|
||||
Key: fact.Key,
|
||||
Value: fact.Value,
|
||||
Confidence: fact.Confidence,
|
||||
Status: models.MemoryStatusActive,
|
||||
}
|
||||
if err := c.stores.Memories.Upsert(ctx, mem); err != nil {
|
||||
log.Printf("⚠ memory compaction upsert failed: key=%s: %v", fact.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("🧠 memory compaction: user %s — %d → %d memories (archived %d)",
|
||||
userID, len(memories), len(merged), archived)
|
||||
|
||||
return &CompactResult{
|
||||
Before: len(memories),
|
||||
After: len(merged),
|
||||
Merged: len(merged),
|
||||
Archived: archived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCompactionResponse parses the model's JSON array response.
|
||||
// Reuses the same extractedFact struct from extractor.go.
|
||||
func parseCompactionResponse(content string) ([]extractedFact, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
// Strip markdown code fences if present
|
||||
if strings.HasPrefix(content, "```") {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) > 2 {
|
||||
content = strings.Join(lines[1:len(lines)-1], "\n")
|
||||
}
|
||||
}
|
||||
|
||||
var facts []extractedFact
|
||||
if err := json.Unmarshal([]byte(content), &facts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return facts, nil
|
||||
}
|
||||
@@ -95,45 +95,81 @@ func (s *Scanner) scan() {
|
||||
// If key is missing (err != nil or val == nil), extraction is enabled.
|
||||
}
|
||||
|
||||
// ── Extraction ──
|
||||
candidates, err := s.findCandidates(ctx)
|
||||
if err != nil {
|
||||
log.Printf("⚠ memory scanner: find candidates: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
if len(candidates) > 0 {
|
||||
sem := make(chan struct{}, s.cfg.Concurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, c := range candidates {
|
||||
key := c.ChannelID + ":" + c.UserID
|
||||
if _, loaded := s.inFlight.LoadOrStore(key, true); loaded {
|
||||
continue
|
||||
}
|
||||
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
|
||||
go func(cand candidate) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
defer s.inFlight.Delete(cand.ChannelID + ":" + cand.UserID)
|
||||
|
||||
extractCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := s.extractor.Extract(extractCtx, cand.ChannelID, cand.UserID, cand.TeamID, cand.PersonaID); err != nil {
|
||||
log.Printf("⚠ memory extraction failed: channel=%s user=%s: %v",
|
||||
cand.ChannelID, cand.UserID, err)
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ── v0.30.1: Compaction (decay + prune) ──
|
||||
// Runs after extraction on every tick. Gated by memory_compaction_enabled.
|
||||
s.runCompaction(ctx)
|
||||
}
|
||||
|
||||
// runCompaction applies confidence decay and pruning to stale memories.
|
||||
func (s *Scanner) runCompaction(ctx context.Context) {
|
||||
if s.stores.Memories == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Semaphore for concurrency control
|
||||
sem := make(chan struct{}, s.cfg.Concurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, c := range candidates {
|
||||
key := c.ChannelID + ":" + c.UserID
|
||||
if _, loaded := s.inFlight.LoadOrStore(key, true); loaded {
|
||||
continue // already in progress
|
||||
}
|
||||
|
||||
sem <- struct{}{} // acquire
|
||||
wg.Add(1)
|
||||
|
||||
go func(cand candidate) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }() // release
|
||||
defer s.inFlight.Delete(cand.ChannelID + ":" + cand.UserID)
|
||||
|
||||
extractCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := s.extractor.Extract(extractCtx, cand.ChannelID, cand.UserID, cand.TeamID, cand.PersonaID); err != nil {
|
||||
log.Printf("⚠ memory extraction failed: channel=%s user=%s: %v",
|
||||
cand.ChannelID, cand.UserID, err)
|
||||
// Check compaction kill switch (default: enabled)
|
||||
if s.stores.GlobalConfig != nil {
|
||||
val, err := s.stores.GlobalConfig.Get(ctx, "memory_compaction_enabled")
|
||||
if err == nil && val != nil {
|
||||
if enabled, ok := val["value"].(bool); ok && !enabled {
|
||||
return
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Decay: reduce confidence for memories not updated in 7 days
|
||||
olderThan := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(time.RFC3339)
|
||||
decayed, err := s.stores.Memories.DecayConfidence(ctx, olderThan, 0.9)
|
||||
if err != nil {
|
||||
log.Printf("⚠ memory compaction: decay failed: %v", err)
|
||||
} else if decayed > 0 {
|
||||
log.Printf("🧠 memory compaction: decayed %d memories", decayed)
|
||||
}
|
||||
|
||||
// Prune: archive memories with confidence below 0.15
|
||||
pruned, err := s.stores.Memories.Prune(ctx, 0.15)
|
||||
if err != nil {
|
||||
log.Printf("⚠ memory compaction: prune failed: %v", err)
|
||||
} else if pruned > 0 {
|
||||
log.Printf("🧠 memory compaction: pruned %d memories", pruned)
|
||||
}
|
||||
}
|
||||
|
||||
// findCandidates queries for channels with enough new activity since last extraction.
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
{{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}}
|
||||
</script>
|
||||
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/sb.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/sb.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/app-state.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/api.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}">
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-admin.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-handlers.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/persona-kb.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/memory-ui.js?v={{.Version}}"></script>
|
||||
|
||||
@@ -515,6 +515,7 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-settings.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-admin.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/tokens.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/note-panel.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notes.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/note-graph.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/files.js?v={{$.Version}}"></script>
|
||||
@@ -522,12 +523,12 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/knowledge-ui.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/memory-ui.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/persona-kb.js?v={{$.Version}}"></script>
|
||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/projects-ui.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/projects-ui.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/channel-models.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notification-prefs.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notifications.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/chat.js?v={{$.Version}}"></script>
|
||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/settings-handlers.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/settings-handlers.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/admin-handlers.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-api.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-admin.js?v={{$.Version}}"></script>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/channel-models.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/tokens.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/extensions.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-panel.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notes.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-graph.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
|
||||
@@ -37,7 +38,7 @@
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notifications.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/panels.js?v={{.Version}}"></script>
|
||||
{{/* debug.js now loaded in base.html for all surfaces */}}
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/projects-ui.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/projects-ui.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/app.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// Notes surface boot: open notes panel in standalone mode
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
{{/* Scripts */}}
|
||||
{{define "scripts-settings"}}
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/task-settings.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/git-credentials-ui.js?v={{.Version}}"></script>
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
|
||||
|
||||
@@ -301,3 +301,36 @@ func (s *MemoryStore) UpsertExtractionLog(ctx context.Context, channelID, userID
|
||||
`, channelID, userID, lastMessageID, count)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── v0.30.1 — Memory Compaction ────────────────────────────────────────
|
||||
|
||||
func (s *MemoryStore) DecayConfidence(ctx context.Context, olderThan string, decayFactor float64) (int, error) {
|
||||
res, err := DB.ExecContext(ctx, `
|
||||
UPDATE memories
|
||||
SET confidence = confidence * $1,
|
||||
updated_at = now()
|
||||
WHERE status = 'active'
|
||||
AND updated_at < $2::timestamptz
|
||||
AND confidence > 0.1
|
||||
`, decayFactor, olderThan)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Prune(ctx context.Context, belowConfidence float64) (int, error) {
|
||||
res, err := DB.ExecContext(ctx, `
|
||||
UPDATE memories
|
||||
SET status = 'archived',
|
||||
updated_at = now()
|
||||
WHERE status = 'active'
|
||||
AND confidence < $1
|
||||
`, belowConfidence)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
@@ -285,3 +285,36 @@ func (s *MemoryStore) UpsertExtractionLog(ctx context.Context, channelID, userID
|
||||
`, store.NewID(), channelID, userID, lastMessageID, count)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── v0.30.1 — Memory Compaction ────────────────────────────────────────
|
||||
|
||||
func (s *MemoryStore) DecayConfidence(ctx context.Context, olderThan string, decayFactor float64) (int, error) {
|
||||
res, err := DB.ExecContext(ctx, `
|
||||
UPDATE memories
|
||||
SET confidence = confidence * ?,
|
||||
updated_at = datetime('now')
|
||||
WHERE status = 'active'
|
||||
AND updated_at < ?
|
||||
AND confidence > 0.1
|
||||
`, decayFactor, olderThan)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Prune(ctx context.Context, belowConfidence float64) (int, error) {
|
||||
res, err := DB.ExecContext(ctx, `
|
||||
UPDATE memories
|
||||
SET status = 'archived',
|
||||
updated_at = datetime('now')
|
||||
WHERE status = 'active'
|
||||
AND confidence < ?
|
||||
`, belowConfidence)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
@@ -64,4 +64,15 @@ type MemoryStore interface {
|
||||
|
||||
// UpsertExtractionLog creates or updates the memory extraction log entry.
|
||||
UpsertExtractionLog(ctx context.Context, channelID, userID, lastMessageID string, count int) error
|
||||
|
||||
// ── v0.30.1 — Memory Compaction ──
|
||||
|
||||
// DecayConfidence reduces confidence for active memories not updated since olderThan.
|
||||
// Formula: new_confidence = confidence * decayFactor. Skips memories with confidence <= 0.1.
|
||||
// Returns the number of affected rows.
|
||||
DecayConfidence(ctx context.Context, olderThan string, decayFactor float64) (int, error)
|
||||
|
||||
// Prune archives active memories whose confidence has dropped below the threshold.
|
||||
// Returns the number of archived rows.
|
||||
Prune(ctx context.Context, belowConfidence float64) (int, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user