Phase 14: dreaming as an opt-in, budgeted, non-acting idle mode

- dreaming.Service: deterministic memory replay sample keyed by dream id,
  vocabulary-based association of unlinked memory pairs (similar_to edges +
  dream_association events), speculative hypotheses as hypothesis-kind beliefs
  with two weak memory citations (starts ~0.30, asserted <= 0.35)
- Run marks the context forbidden for capabilities before anything else;
  test proves Invoke refuses inside a dream
- per-mind settings: enabled (default off), hours window (wraps midnight),
  interval, model-call / memory / hypothesis caps; global pause switch
- optional model wording via new `dream` prompt (role hypothesise)
- promotion adds operator evidence, rejection retracts the belief; both
  audited and recorded as operator actions
- migration 0014, API, Dreams tab and page, noemactl dreams, docs/dreaming.md
This commit is contained in:
Andy Dixon
2026-07-01 21:12:13 +01:00
parent 89edbb7c06
commit 2ac8752bac
20 changed files with 1217 additions and 3 deletions
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"fmt"
"os"
"strings"
"text/tabwriter"
)
func init() {
extraCommands = append(extraCommands,
command{"dreams", "<mind> list|run|promote <id> [note]|reject <id> [reason] – dreaming and its hypotheses", cmdDreams},
)
}
func cmdDreams(args []string) int {
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: noemactl dreams <mind> list | run | promote <id> [note] | reject <id> [reason]")
return exitUsage
}
mind, base := args[0], "/api/v1/minds/"+args[0]+"/dreams"
switch args[1] {
case "list":
var out struct {
Items []map[string]any
Hypotheses []map[string]any
Paused bool
Settings map[string]any
}
if code, err := apiRequest("GET", base, nil, &out); err != nil {
return exitFor(code, err)
}
fmt.Printf("mind %s · enabled=%v hours=%v interval=%v paused=%v\n", mind, out.Settings["enabled"], out.Settings["hours"], out.Settings["interval"], out.Paused)
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "WHEN\tSTATUS\tSAMPLED\tLINKS\tHYPOTHESES\tERROR")
for _, d := range out.Items {
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%v\n", d["created_at"], d["status"], d["sampled"], d["associations"], d["hypotheses"], d["error"])
}
tw.Flush()
fmt.Println()
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "STATUS\tCONF\tPROPOSITION\tID")
for _, h := range out.Hypotheses {
fmt.Fprintf(tw, "%v\t%.2f\t%v\t%v\n", h["status"], h["confidence"], h["proposition"], h["id"])
}
tw.Flush()
return exitOK
case "run":
var d map[string]any
if code, err := apiRequest("POST", base+"/run", nil, &d); err != nil {
return exitFor(code, err)
}
fmt.Printf("%v: sampled=%v links=%v hypotheses=%v %v\n", d["status"], d["sampled"], d["associations"], d["hypotheses"], d["error"])
return exitOK
case "promote", "reject":
if len(args) < 3 {
return exitUsage
}
var h map[string]any
if code, err := apiRequest("POST", base+"/hypotheses/"+args[2]+"/"+args[1], map[string]any{"note": strings.Join(args[3:], " ")}, &h); err != nil {
return exitFor(code, err)
}
fmt.Printf("%v (%.2f)\n", h["status"], h["confidence"])
return exitOK
}
return exitUsage
}
+11
View File
@@ -24,6 +24,7 @@ import (
"github.com/andydixon/noema/internal/config"
"github.com/andydixon/noema/internal/consolidation"
"github.com/andydixon/noema/internal/conversation"
"github.com/andydixon/noema/internal/dreaming"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/goals"
"github.com/andydixon/noema/internal/graph"
@@ -149,6 +150,7 @@ func run() error {
deps.Narrative = narrative.NewStore(db)
deps.Notifications = notifications.NewStore(db)
deps.Consolidation = &consolidation.Service{DB: db, Memory: deps.Memory, Beliefs: deps.Beliefs, Events: deps.Events, LLM: deps.LLM, Prompts: deps.Prompts, Minds: deps.Minds, Log: log}
deps.Dreaming = &dreaming.Service{DB: db, Memory: deps.Memory, Beliefs: deps.Beliefs, Events: deps.Events, LLM: deps.LLM, Prompts: deps.Prompts, Log: log}
deps.Reflection = &reflection.Service{DB: db, Events: deps.Events, Beliefs: deps.Beliefs, Goals: deps.Goals, Memory: deps.Memory, Predictions: deps.Predictions, LLM: deps.LLM, Prompts: deps.Prompts, Log: log}
registry := brain.NewRegistry().AddProcess(cognition.Attend{}).AddProcess(cognition.Retrieve{}).AddProcess(cognition.Associate{}).AddProcess(cognition.Perceive{}).
AddProcess(cognition.GoalMonitor{}).AddProcess(cognition.Curiosity{}).AddProcess(cognition.Metacognition{}).AddProcess(cognition.Act{}).
@@ -290,6 +292,15 @@ func backgroundCognition(ctx context.Context, d app.Deps) error {
d.Brain.Command(ctx, m.ID, "setmode", map[string]bool{"awake": true}) //nolint:errcheck
}
}
if m.Config.DreamingEnabled && !dreaming.IsPaused() && dreaming.InWindow(m.Config.DreamHours, time.Now()) {
if ds, _ := d.Dreaming.Dreams(ctx, m.ID, 1); len(ds) == 0 || time.Since(ds[0].CreatedAt) >= m.Config.DreamInterval.D() {
d.Brain.Command(ctx, m.ID, "setmode", map[string]bool{"dreaming": true}) //nolint:errcheck
if _, err := d.Dreaming.Run(ctx, m, "schedule", "noemad"); err != nil {
d.Log.Warn("dream failed", "mind", m.Slug, "err", err)
}
d.Brain.Command(ctx, m.ID, "setmode", map[string]bool{"awake": true}) //nolint:errcheck
}
}
if m.Config.ReflectionEnabled {
if last, err := d.Reflection.Latest(ctx, m.ID); err != nil || time.Since(last.CreatedAt) >= m.Config.ReflectionInterval.D() {
d.Brain.Command(ctx, m.ID, "setmode", map[string]bool{"reflecting": true}) //nolint:errcheck