Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
bin/
dist/

# Database / local cache
# Database / local cache (SQLite main DB + WAL/SHM sidecars)
elrond*.db
*.codegraph.db
*.db-wal
*.db-shm
.codegraph/*.db

# Local yaad runtime state (key material + SQLite database + local config — never commit)
.yaad/
.yaad/integrity.key
.yaad/yaad.db
.yaad/config.toml
Expand Down
99 changes: 69 additions & 30 deletions compact/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package compact
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"log/slog"
"strings"

"github.com/GrayCodeAI/yaad/storage"
Expand Down Expand Up @@ -93,6 +95,8 @@ func (c *Compactor) Compact(ctx context.Context, project string) (int, error) {

summary, err := c.summarizer.Summarize(ctx, typ, contents)
if err != nil {
// Skipping this group is safe — nothing has been mutated yet.
slog.Warn("compact: summarize failed, skipping group", "type", typ, "count", len(group), "error", err)
continue
}

Expand All @@ -112,61 +116,96 @@ func (c *Compactor) Compact(ctx context.Context, project string) (int, error) {
Version: 1,
}
if err := c.store.CreateNode(ctx, summaryNode); err != nil {
// Skipping this group is safe — nothing else has been mutated yet.
slog.Warn("compact: create summary node failed, skipping group",
"type", typ, "summary_node_id", summaryNode.ID, "error", err)
continue
}

// Re-link edges: transfer edges from compacted nodes to the summary node.
// From here on the summary node exists, so failures would leave the graph
// partially re-linked — abort and propagate instead of silently continuing.
compactedIDs := make(map[string]bool, len(ids))
for _, id := range ids {
compactedIDs[id] = true
}
for _, id := range ids {
// Outbound edges: compacted → other
if outEdges, err := c.store.GetEdgesFrom(ctx, id); err == nil {
for _, e := range outEdges {
if compactedIDs[e.ToID] {
continue // skip edges between compacted nodes
}
_ = c.store.CreateEdge(ctx, &storage.Edge{
ID: uuid.New().String(),
FromID: summaryNode.ID,
ToID: e.ToID,
Type: e.Type,
Weight: e.Weight,
})
outEdges, err := c.store.GetEdgesFrom(ctx, id)
if err != nil {
return compacted, fmt.Errorf("compact: list outbound edges of node %s: %w", id, err)
}
for _, e := range outEdges {
if compactedIDs[e.ToID] {
continue // skip edges between compacted nodes
}
if err := c.relinkEdge(ctx, summaryNode.ID, e.ToID, e); err != nil {
return compacted, err
}
}
// Inbound edges: other → compacted
if inEdges, err := c.store.GetEdgesTo(ctx, id); err == nil {
for _, e := range inEdges {
if compactedIDs[e.FromID] {
continue
}
_ = c.store.CreateEdge(ctx, &storage.Edge{
ID: uuid.New().String(),
FromID: e.FromID,
ToID: summaryNode.ID,
Type: e.Type,
Weight: e.Weight,
})
inEdges, err := c.store.GetEdgesTo(ctx, id)
if err != nil {
return compacted, fmt.Errorf("compact: list inbound edges of node %s: %w", id, err)
}
for _, e := range inEdges {
if compactedIDs[e.FromID] {
continue
}
if err := c.relinkEdge(ctx, e.FromID, summaryNode.ID, e); err != nil {
return compacted, err
}
}
}

// Archive compacted nodes
for _, id := range ids {
old, _ := c.store.GetNode(ctx, id)
if old != nil {
_ = c.store.SaveVersion(ctx, old.ID, old.Content, "compactor", "compacted into "+summaryNode.ID[:8])
old.Confidence = 0
_ = c.store.UpdateNode(ctx, old)
compacted++
old, err := c.store.GetNode(ctx, id)
if err != nil {
if errors.Is(err, storage.ErrNodeNotFound) {
// Node disappeared concurrently — nothing to archive.
slog.Warn("compact: node vanished before archival", "node_id", id)
continue
}
return compacted, fmt.Errorf("compact: load node %s for archival: %w", id, err)
}
if old == nil {
continue
}
if err := c.store.SaveVersion(ctx, old.ID, old.Content, "compactor", "compacted into "+summaryNode.ID[:8]); err != nil {
return compacted, fmt.Errorf("compact: save version of node %s: %w", old.ID, err)
}
old.Confidence = 0
if err := c.store.UpdateNode(ctx, old); err != nil {
return compacted, fmt.Errorf("compact: archive node %s: %w", old.ID, err)
}
compacted++
}
}
return compacted, nil
}

// relinkEdge transfers an edge onto the summary node. Duplicate edges are
// benign (the link already exists) and are logged at debug level; any other
// failure is propagated so the compaction pipeline can abort.
func (c *Compactor) relinkEdge(ctx context.Context, fromID, toID string, orig *storage.Edge) error {
err := c.store.CreateEdge(ctx, &storage.Edge{
ID: uuid.New().String(),
FromID: fromID,
ToID: toID,
Type: orig.Type,
Weight: orig.Weight,
})
if err == nil {
return nil
}
if errors.Is(err, storage.ErrDuplicateEdge) {
slog.Debug("compact: re-linked edge already exists", "from", fromID, "to", toID, "type", orig.Type)
return nil
}
return fmt.Errorf("compact: re-link edge %s→%s (%s): %w", fromID, toID, orig.Type, err)
}

func buildCompactSummary(typ string, contents []string) string {
// Take first 5 items as representative
limit := 5
Expand Down
16 changes: 13 additions & 3 deletions temporal/backbone.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package temporal

import (
"context"
"log/slog"
"sync"

"github.com/GrayCodeAI/yaad/storage"
Expand Down Expand Up @@ -105,21 +106,30 @@ func (b *Backbone) recoverTail(ctx context.Context, project string) string {
}

// persistTail saves the tail node ID as a metadata node for O(1) recovery.
// It is best-effort (often invoked from a goroutine): failures degrade tail
// recovery to the slow scan path, so they are logged rather than propagated —
// but never silently discarded.
func (b *Backbone) persistTail(ctx context.Context, project, nodeID string) {
key := "_temporal_tail"
if existing, err := b.store.GetNodeByKey(ctx, key, project); err == nil && existing != nil {
_ = b.store.UpdateNodeContent(ctx, existing.ID, nodeID)
if uerr := b.store.UpdateNodeContent(ctx, existing.ID, nodeID); uerr != nil {
slog.Warn("temporal: failed to update tail marker (recovery will fall back to scan)",
"project", project, "marker_id", existing.ID, "tail_node_id", nodeID, "error", uerr)
}
return
}
_ = b.store.CreateNode(ctx, &storage.Node{
if cerr := b.store.CreateNode(ctx, &storage.Node{
ID: uuid.New().String(),
Type: "meta",
Content: nodeID,
Key: key,
Project: project,
Scope: "system",
Tier: 1,
})
}); cerr != nil {
slog.Warn("temporal: failed to create tail marker (recovery will fall back to scan)",
"project", project, "tail_node_id", nodeID, "error", cerr)
}
}

// Timeline returns nodes in chronological order for a project,
Expand Down
Loading