diff --git a/.gitignore b/.gitignore index 0bc98d9..1b8f4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/compact/compact.go b/compact/compact.go index d024b07..209b6ad 100644 --- a/compact/compact.go +++ b/compact/compact.go @@ -5,7 +5,9 @@ package compact import ( "context" "crypto/sha256" + "errors" "fmt" + "log/slog" "strings" "github.com/GrayCodeAI/yaad/storage" @@ -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 } @@ -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 diff --git a/temporal/backbone.go b/temporal/backbone.go index 8865da0..efddffd 100644 --- a/temporal/backbone.go +++ b/temporal/backbone.go @@ -10,6 +10,7 @@ package temporal import ( "context" + "log/slog" "sync" "github.com/GrayCodeAI/yaad/storage" @@ -105,13 +106,19 @@ 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, @@ -119,7 +126,10 @@ func (b *Backbone) persistTail(ctx context.Context, project, nodeID string) { 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,