// Command socialcuriosity explores the Nostr social graph to feed iskra's // morpheme lattice. Starting from a seed npub, it fetches all kind-1 notes, // digests them through the morpheme enzyme, and follows interaction edges // (reply threads, mentions) to discover new authors. // // When the bond rate for the current npub drops, it looks at who they // corresponded with and picks the most promising unvisited npub. The lattice // grows dynamically when occupancy exceeds 80%. package main import ( "context" "crypto/rand" "encoding/binary" "encoding/json" "flag" "fmt" "log" "os" "os/signal" "strings" "time" "git.mleku.dev/mleku/dendrite/pkg/axiom" "git.mleku.dev/mleku/dendrite/pkg/forage" "git.mleku.dev/mleku/dendrite/pkg/grammar" "git.mleku.dev/mleku/dendrite/pkg/grow" "git.mleku.dev/mleku/dendrite/pkg/lattice" "git.mleku.dev/mleku/dendrite/pkg/mindsicle" "git.mleku.dev/mleku/dendrite/pkg/nostr" "git.mleku.dev/mleku/dendrite/pkg/reflex" "git.mleku.dev/mleku/dendrite/pkg/relay" ) type socialCheckpoint struct { Round int `json:"round"` CurrentPubkey string `json:"current_pubkey"` SeenNpubs map[string]*forage.NpubScore `json:"seen_npubs"` AllInteractions map[string][]forage.InteractionEdge `json:"all_interactions"` TotalBonded int `json:"total_bonded"` BondRateEWMA float64 `json:"bond_rate_ewma"` ZeroStreak int `json:"zero_streak"` Seed uint64 `json:"seed"` SavedAt string `json:"saved_at"` } func main() { var ( npubFlag = flag.String("npub", "", "seed npub (required)") relayFlag = flag.String("relay", "wss://relay.orly.dev", "seed relay URL") latticePath = flag.String("lattice", ".condense_morpheme_fast_db.mindsicle.bin", "path to morpheme mindsicle") arcsPath = flag.String("arcs", ".condense_rune_db.arcs.bin", "path to reflex arcs binary") maxEvents = flag.Int("max-events", 500, "max events to fetch per npub") minContent = flag.Int("min-content", 30, "minimum note content length in bytes") zeroLimit = flag.Int("zero-streak", 5, "stop after N consecutive zero-bond npubs") fetchRate = flag.Duration("rate", 3*time.Second, "delay between relay requests") seed = flag.Uint64("seed", 0, "random seed (0=time-based)") outputDB = flag.String("output", ".socialcuriosity_db", "checkpoint file prefix") journalPath = flag.String("journal", "socialcuriosity_journal.log", "audit log path") ) flag.Parse() if *npubFlag == "" { log.Fatal("socialcuriosity: -npub is required") } seedHex, err := nostr.NpubToHex(*npubFlag) if err != nil { log.Fatalf("socialcuriosity: invalid npub: %v", err) } log.Printf("socialcuriosity: seed pubkey %s", seedHex) if *seed == 0 { *seed = uint64(time.Now().UnixNano()) } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() // --- Load reflex arcs --- log.Printf("socialcuriosity: loading arcs from %s", *arcsPath) arcFile, err := os.Open(*arcsPath) if err != nil { log.Fatalf("socialcuriosity: cannot open arcs: %v", err) } arcs, err := reflex.Load(arcFile) arcFile.Close() if err != nil { log.Fatalf("socialcuriosity: cannot load arcs: %v", err) } log.Printf("socialcuriosity: loaded %d arcs", len(arcs)) arcIdx := reflex.BuildArcIndex(arcs) enzyme := reflex.NewMorphemeEnzyme(arcIdx) cf := func(tag string) axiom.Constraint { return grammar.NewConstraint(tag, grammar.MorphemeText) } // --- Load lattice --- log.Printf("socialcuriosity: loading lattice from %s", *latticePath) f, err := os.Open(*latticePath) if err != nil { log.Fatalf("socialcuriosity: cannot open lattice: %v", err) } lat, _, err := mindsicle.StreamThaw(f, cf) f.Close() if err != nil { log.Fatalf("socialcuriosity: cannot thaw lattice: %v", err) } h := lat.Health() log.Printf("socialcuriosity: lattice loaded: %d/%d occupied (%.1f%%)", h.Occupied, h.NodeCount, h.OccupancyRate.Float64()*100) lat.BuildAndAttachWalkColumns() wc := lat.GetWalkColumns() // --- Load or create checkpoint --- ckptPath := *outputDB + ".checkpoint.json" var ckpt socialCheckpoint if data, err := os.ReadFile(ckptPath); err == nil { if err := json.Unmarshal(data, &ckpt); err == nil { log.Printf("socialcuriosity: resumed from round %d (%d npubs seen, streak=%d)", ckpt.Round, len(ckpt.SeenNpubs), ckpt.ZeroStreak) *seed = ckpt.Seed } } if ckpt.SeenNpubs == nil { ckpt.SeenNpubs = make(map[string]*forage.NpubScore) } if ckpt.AllInteractions == nil { ckpt.AllInteractions = make(map[string][]forage.InteractionEdge) } ckpt.Seed = *seed if ckpt.CurrentPubkey == "" { ckpt.CurrentPubkey = seedHex } // --- Open journal --- journal, err := os.OpenFile(*journalPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { log.Fatalf("socialcuriosity: cannot open journal: %v", err) } defer journal.Close() fmt.Fprintf(journal, "# socialcuriosity session started at %s (seed=%d, npub=%s)\n", time.Now().Format(time.RFC3339), *seed, *npubFlag) // --- Create relay pool --- pool := relay.NewPool(*relayFlag) log.Printf("socialcuriosity: relay pool seeded with %s", *relayFlag) // --- Main loop --- startRound := ckpt.Round + 1 log.Printf("socialcuriosity: starting from round %d (current=%s, zero-limit=%d)", startRound, ckpt.CurrentPubkey[:16], *zeroLimit) for round := startRound; ; round++ { select { case <-ctx.Done(): log.Printf("socialcuriosity: interrupted at round %d, saving", round) saveCheckpoint(ckptPath, ckpt) saveLattice(lat, *latticePath) return default: } currentHex := ckpt.CurrentPubkey npubStr, _ := nostr.PubkeyToNpub(currentHex) log.Printf("socialcuriosity: round %d: feeding from %s (%s)", round, npubStr, currentHex[:16]) // 1. Fetch events for current npub. events, sourceRelay := pool.FetchEventsMultiRelay( ctx, currentHex, *maxEvents, *minContent, 15*time.Second) if len(events) == 0 { log.Printf("socialcuriosity: round %d: no events found for %s", round, currentHex[:16]) ckpt.ZeroStreak++ } else { log.Printf("socialcuriosity: round %d: fetched %d events from %s", round, len(events), sourceRelay) // Deduplicate by event ID. events = dedup(events) // 2. Digest each note. score, ok := ckpt.SeenNpubs[currentHex] if !ok { score = &forage.NpubScore{Pubkey: currentHex} ckpt.SeenNpubs[currentHex] = score } roundBonded := 0 roundElems := 0 for _, ev := range events { elems := enzyme.DigestText(ev.Content) bonded := feedElements(ctx, lat, elems) score.UpdateBondRate(len(elems), bonded, len(ev.Content)) roundBonded += bonded roundElems += len(elems) } ckpt.TotalBonded += roundBonded // Session EWMA. sessionRate := 0.0 if roundElems > 0 { sessionRate = float64(roundBonded) / float64(roundElems) } ckpt.BondRateEWMA = 0.8*ckpt.BondRateEWMA + 0.2*sessionRate if roundBonded == 0 { ckpt.ZeroStreak++ } else { ckpt.ZeroStreak = 0 } log.Printf("socialcuriosity: round %d: %d notes, %d morphemes, %d bonded (%.2f%%), streak=%d", round, len(events), roundElems, roundBonded, sessionRate*100, ckpt.ZeroStreak) // 3. Extract interactions. interactions := forage.ExtractInteractions(events, currentHex) ckpt.AllInteractions[currentHex] = interactions if len(interactions) > 0 { log.Printf("socialcuriosity: round %d: %d interaction edges from %s", round, len(interactions), currentHex[:16]) } // 4. Journal. fmt.Fprintf(journal, "%s round=%d pubkey=%s npub=%s relay=%s events=%d elems=%d bonded=%d rate=%.2f%% bonds_per_kb=%.1f streak=%d interactions=%d\n", time.Now().Format(time.RFC3339), round, currentHex, npubStr, sourceRelay, len(events), roundElems, roundBonded, sessionRate*100, score.BondsPerKB, ckpt.ZeroStreak, len(interactions)) } // 5. Dissolve weak bonds. dissolved := dissolveWeak(lat) if dissolved > 0 { log.Printf("socialcuriosity: dissolved %d weak bonds", dissolved) } // 6. Update health. h = lat.Health() log.Printf("socialcuriosity: health: %dK/%dK (%.1f%%)", h.Occupied/1000, h.NodeCount/1000, h.OccupancyRate.Float64()*100) // 7. Dynamic growth at 80% occupancy. if h.OccupancyRate.Float64() > 0.80 { growBy := h.NodeCount / 2 var expandSeed [32]byte binary.LittleEndian.PutUint64(expandSeed[:], *seed+uint64(round)*0xdead) rand.Read(expandSeed[8:]) added := grammar.ExpandLattice(lat, grammar.MorphemeText, growBy, expandSeed, cf, grammar.MorphemeDefaultCounts) if added > 0 { lat.BuildAndAttachWalkColumns() wc = lat.GetWalkColumns() log.Printf("socialcuriosity: expanded lattice by %d nodes (now %d)", added, lat.Size()) ckpt.ZeroStreak = 0 } } // 8. Decide whether to switch npubs. // Walk lattice weak regions for baby talk (used as tiebreaker). walkSeed := *seed + uint64(round)*0xbeef walkedValues := forage.WeakRegionWalk(lat, wc, 5000, walkSeed) babyTalk := forage.ExtractBabyTalk(walkedValues) // Merge all interactions for scoring. var allEdges []forage.InteractionEdge for _, edges := range ckpt.AllInteractions { allEdges = append(allEdges, edges...) } nextHex := forage.ScoreNextNpub(allEdges, ckpt.SeenNpubs, babyTalk) if nextHex != "" && nextHex != currentHex { nextNpub, _ := nostr.PubkeyToNpub(nextHex) log.Printf("socialcuriosity: switching to %s (%s)", nextNpub, nextHex[:16]) ckpt.CurrentPubkey = nextHex } else if len(events) == 0 || (ckpt.SeenNpubs[currentHex] != nil && ckpt.SeenNpubs[currentHex].BondRateEWMA < 0.0001) { // Current npub exhausted and no candidates. if nextHex == "" { log.Printf("socialcuriosity: no unvisited npubs remaining") ckpt.ZeroStreak = *zeroLimit // force convergence } } // 9. Checkpoint every 5 rounds. ckpt.Round = round if round%5 == 0 { saveCheckpoint(ckptPath, ckpt) saveLattice(lat, *latticePath) log.Printf("socialcuriosity: checkpoint saved at round %d", round) } // 10. Check convergence. if ckpt.ZeroStreak >= *zeroLimit { log.Printf("socialcuriosity: converged — %d consecutive zero-bond rounds", ckpt.ZeroStreak) break } // Rate limit. select { case <-time.After(*fetchRate): case <-ctx.Done(): } } // Final save. saveCheckpoint(ckptPath, ckpt) saveLattice(lat, *latticePath) h = lat.Health() fmt.Fprintf(journal, "# socialcuriosity session ended at %s: %d rounds, %d npubs visited, %d total bonded, %dK/%dK occupied (%.1f%%)\n", time.Now().Format(time.RFC3339), ckpt.Round, len(ckpt.SeenNpubs), ckpt.TotalBonded, h.Occupied/1000, h.NodeCount/1000, h.OccupancyRate.Float64()*100) log.Printf("socialcuriosity: done — %d rounds, %d npubs, %d total bonded", ckpt.Round, len(ckpt.SeenNpubs), ckpt.TotalBonded) log.Printf("%s", pool.Stats()) } // feedElements feeds morpheme elements into the lattice via grow.Run. func feedElements(ctx context.Context, lat *lattice.Lattice, elems []axiom.Element) int { if len(elems) == 0 { return 0 } ch := make(chan axiom.Element, len(elems)) go func() { for _, e := range elems { ch <- e } close(ch) }() cfg := grow.Config{ MaxSteps: 10, Workers: grow.WorkerCount(), } events := make(chan grow.Event, 256) bonded := 0 done := make(chan struct{}) go func() { for ev := range events { if ev.Type == grow.EventBonded { bonded++ } } close(done) }() grow.Run(ctx, lat, ch, cfg, events) close(events) <-done return bonded } // dissolveWeak removes occupants with low contextual lock-in. func dissolveWeak(l *lattice.Lattice) int { nodes := l.Nodes() target := max(1, len(nodes)/100) type scored struct { node *lattice.Node score float64 } var weak []scored for _, n := range nodes { if !n.Occupied() { continue } s := n.ContextualLockIn().Float64() if s < 0.3 { weak = append(weak, scored{n, s}) } } dissolved := 0 for _, s := range weak { if dissolved >= target { break } s.node.Dissolve() l.ReindexVacant(s.node) dissolved++ } return dissolved } // dedup removes duplicate events by ID. func dedup(events []*nostr.Event) []*nostr.Event { seen := make(map[string]bool, len(events)) out := make([]*nostr.Event, 0, len(events)) for _, ev := range events { if !seen[ev.ID] { seen[ev.ID] = true out = append(out, ev) } } return out } func saveCheckpoint(path string, ckpt socialCheckpoint) { ckpt.SavedAt = time.Now().Format(time.RFC3339) data, err := json.MarshalIndent(ckpt, "", " ") if err != nil { log.Printf("socialcuriosity: WARNING: cannot marshal checkpoint: %v", err) return } tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { log.Printf("socialcuriosity: WARNING: cannot write checkpoint: %v", err) return } os.Rename(tmp, path) } func saveLattice(lat *lattice.Lattice, path string) { tmp := path + ".tmp" f, err := os.Create(tmp) if err != nil { log.Printf("socialcuriosity: WARNING: cannot create %s: %v", tmp, err) return } if err := mindsicle.StreamFreeze(lat, nil, f); err != nil { f.Close() os.Remove(tmp) log.Printf("socialcuriosity: WARNING: save failed: %v", err) return } f.Close() if err := os.Rename(tmp, path); err != nil { log.Printf("socialcuriosity: WARNING: rename failed: %v", err) return } fi, _ := os.Stat(path) if fi != nil { log.Printf("socialcuriosity: saved %.2f MB", float64(fi.Size())/(1024*1024)) } } func truncate(s string, n int) string { s = strings.ReplaceAll(s, "\n", " ") if len(s) > n { return s[:n] + "..." } return s }