// internal/mode/offscreen/collect.go package offscreen import ( "context" "fmt" "strings" "time" "keel/internal/frame" ) const ( recentProposals = 5 // how many recent proposals to show outcomeScan = 50 // how many outcome events to scan for correlation recentWindow = 7 * 24 * time.Hour ) // assembleBrief builds the off-screen prompt context: the shared frame brief // (today's tasks + ~/owc goals and life-domains) plus off-screen's own recent // proposal history. Best-effort: never panics, never errors, tolerates nil // ports and missing files. func (m *Mode) assembleBrief() string { base := frame.Assemble(context.Background(), m.know, m.tasks) hist := m.briefHistory() if hist == "" { return base } var b strings.Builder b.WriteString(base) if !strings.HasSuffix(base, "\n") { b.WriteString("\n") } b.WriteString("\n## Recently proposed\n") b.WriteString(hist) return frame.Truncate(b.String(), frame.MaxBriefBytes) } // briefHistory renders recent proposals with their outcome, newest first and // within recentWindow. Returns "" when there is no memory or no recent history. func (m *Mode) briefHistory() string { if m.mem == nil { return "" } ctx := context.Background() proposals, err := m.mem.Recent(ctx, kindProposalMade, recentProposals) if err != nil || len(proposals) == 0 { return "" } taken := m.idSet(ctx, kindActionTaken) dismissed := m.idSet(ctx, kindProposalDismissed) now := m.now() var b strings.Builder for _, ev := range proposals { if now.Sub(ev.At) > recentWindow { continue } id, _ := ev.Data["proposal_id"].(string) action, _ := ev.Data["next_action"].(string) if action == "" { continue } outcome := "unactioned" if taken[id] { outcome = "confirmed" } else if dismissed[id] { outcome = "dismissed" } fmt.Fprintf(&b, "- %q (%s, %s)\n", action, outcome, relativeAge(now.Sub(ev.At))) } return b.String() } // idSet reads recent events of a kind and returns their proposal_ids as a set. func (m *Mode) idSet(ctx context.Context, kind string) map[string]bool { out := map[string]bool{} evs, err := m.mem.Recent(ctx, kind, outcomeScan) if err != nil { return out } for _, e := range evs { if id, _ := e.Data["proposal_id"].(string); id != "" { out[id] = true } } return out } // relativeAge renders a coarse human age for a duration. func relativeAge(d time.Duration) string { switch { case d < time.Minute: return "just now" case d < time.Hour: return fmt.Sprintf("%dm ago", int(d.Minutes())) case d < 24*time.Hour: return fmt.Sprintf("%dh ago", int(d.Hours())) default: return fmt.Sprintf("%dd ago", int(d.Hours()/24)) } }