59 lines
2.6 KiB
Go
59 lines
2.6 KiB
Go
// This file wires the startup background trigger for /dream memory
|
|
// consolidation (US-008, FR-4/FR-17): when an interactive REPL session starts,
|
|
// if [dream].enabled and a consolidation is due (per dream.State.Due), the dream
|
|
// subprocess is spawned in the BACKGROUND so it never delays the first user
|
|
// response, and a non-intrusive one-line summary is printed on completion.
|
|
//
|
|
// The decision + goroutine live in dream.Scheduler; this file only supplies the
|
|
// CLI-side spawn seam (reusing spawnDream from dream_repl.go) and the one-line
|
|
// notice renderer (RenderReportLine). The subprocess's O_EXCL lock enforces
|
|
// single-instance, so a second trigger just yields a skipped child (silent).
|
|
package repl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/smallnest/pigo/internal/cli/ui"
|
|
"github.com/smallnest/pigo/internal/dream"
|
|
)
|
|
|
|
// dreamStartupScheduler owns the startup auto-trigger decision. It is stateless
|
|
// (dream.Scheduler is a zero-size type), so a package value is enough; tests
|
|
// exercise this path through the spawnDream seam rather than replacing it.
|
|
var dreamStartupScheduler dream.Scheduler
|
|
|
|
// maybeStartBackgroundDream launches an auto-consolidation in the background at
|
|
// interactive session startup when dream is enabled and due. It never blocks:
|
|
// the due check is a single state.json read and any spawn runs in a goroutine,
|
|
// so the first prompt is served immediately (SPEC FR-4 / §8.2). The completion
|
|
// notice is a single dim line via RenderReportLine; a skipped, no-op, or failed
|
|
// run prints nothing (SPEC §6.1).
|
|
//
|
|
// out is written to from the background goroutine, so callers must pass a writer
|
|
// safe for a late async line (the REPL uses os.Stdout, where a one-line notice
|
|
// simply appears in scrollback). It is a no-op when memoryRoot is empty (dream
|
|
// state has nowhere to live) or dream is disabled/not due.
|
|
func maybeStartBackgroundDream(out io.Writer, memoryRoot, projectDir string, cfg dream.Config) bool {
|
|
if memoryRoot == "" {
|
|
return false
|
|
}
|
|
return dreamStartupScheduler.MaybeRunBackground(context.Background(), dream.BackgroundDeps{
|
|
MemoryRoot: memoryRoot,
|
|
ProjectDir: projectDir,
|
|
Config: cfg,
|
|
Spawn: func(ctx context.Context, dir string) (dream.Report, error) {
|
|
// Bound the background run like a manual /dream (SPEC §6.3): a hung
|
|
// LLM-backed pass is killed rather than leaking a goroutine forever.
|
|
ctx, cancel := context.WithTimeout(ctx, dreamRunTimeout)
|
|
defer cancel()
|
|
res, err := spawnDream(ctx, dir, false)
|
|
return res.report, err
|
|
},
|
|
OnReport: func(r dream.Report) {
|
|
fmt.Fprintln(out, ui.Colorize(ui.Enabled(), ui.Dim, RenderReportLine(r)))
|
|
},
|
|
})
|
|
}
|