8.9 KiB
M7 — Reflection: Design
Status: approved Date: 2026-06-01 Milestone: M7 — Reflection (the deferred AI reviewer role, promoted into the main loop)
Purpose
Close the loop. Through M6 the system has three live AI roles — Coach, DriftJudge, Nudge — but nothing looks back. M7 adds the fourth role, the Reviewer: when a session ends, it reflects on what just happened, read against your recent sessions, and produces two short lines —
- a recap, shown on the Review screen (how the session went), and
- a carry-forward, which grounds the coach the next time you plan (what to do differently).
This is the "Focus OS Reflection" step from the roadmap. It makes the loop self-reinforcing: each session's takeaway sharpens the next session's plan.
The whole feature is one cheap async call per session, never blocking, and degrades gracefully — if the AI backend is off or slow, Review/End/Planning behave exactly as they do today.
Design constraints
Two non-negotiables shaped every decision below:
- Efficient. One LLM call per session, fired once on entering Review. The "recent sessions" context is a local file read, not an LLM cost. The prompt carries the finished session plus a few compact prior summaries (outcome + top buckets, never raw event logs), so it stays small. The result is computed once and persisted; the next planning cycle reads it from disk and does not re-run the reviewer.
- Low friction. It runs automatically (no "request reflection" button). It is never blocking — the End button works immediately whether or not the reviewer has returned. The carry-forward is auto-applied as coach grounding next time; there is no approve/dismiss step.
The new AI role (ai package)
A leaf role that mirrors Coach/DriftJudge/Nudge: it takes only primitives and
imports neither store nor session. The controller is responsible for turning
session data into the strings this role consumes.
// Reflection is the reviewer's output: two short, single-line fields.
type Reflection struct {
Recap string // backward-looking, ≤1 short line — shown on Review
CarryForward string // forward-looking, ≤1 short line — grounds the next
// coach and is shown on the next Planning screen
}
// Review reflects on a just-finished session, read against recent history.
// finished: a compact description of the session that just ended.
// history: a compact description of the last few prior sessions ("" if none).
func (b *Backend) Review(ctx context.Context, finished, history string) (Reflection, error)
- A new prompt builder composes a Reviewer prompt from
finishedandhistory, instructing the model to return at most one short line per field so output stays bounded. - A parser extracts the two lines from the backend output, following the
existing role-parsing pattern in the
aipackage. - Graceful fallback: any error, empty output, or unparseable result yields a
zero
Reflection{}(both fields ""), which the rest of the system treats as "no reflection available."Reviewnever panics and never blocks.
The prompt is built so that an empty history (the first-ever session) still
produces a sensible recap from finished alone.
Orchestration (session.Controller)
The controller owns all orchestration, reusing the established async + generation-counter + graceful-degradation pattern already used for the coach, tasks, and knowledge fetches.
Fetch on entering Review
enterReview (reached from both Complete → completed and Expire →
expired) fires startReflectionFetchLocked():
- Increment a
reflectionGencounter and capture it for this fetch. - Build
finishedfrom the in-memory frozen stats of the session that just ended: next action, success condition, outcome, switch count, and the top app time buckets. - Build
historyby reading the last 5 priorSessionSummaryrecords fromaudit.jsonl. The just-finished session is not in the chain yet — it is appended only atEnd— so there is no double-counting. - In a goroutine, call
reviewer.Review(ctx, finished, history)under a timeout. On return, re-acquire the lock; ifreflectionGenstill matches the captured value, cachereflectionRecapand setcarryForward(latest-wins); otherwise discard the result as stale. Then notify.
The generation guard ensures a slow review from a superseded session can never overwrite a newer one: the most recent completed review wins.
Grounding the next coach — no interface change
grounding is already a free-form string parameter on ai.Coach (added in M6).
M7 needs no change to the ai.Coach signature: in RequestCoach the
controller composes the existing knowledge profile text and the current
carryForward into that one grounding string (profile block, then a short
"Last session:" line). M7 is fully additive to the AI interface.
carryForward is latest-wins and survives End (it is not cleared with the
commitment/stats), so it is present when the next Planning begins.
State projection
The State view gains a small reflection projection so the browser can render it:
type ReflectionView struct {
Status string // "idle" | "pending" | "ready" | "absent"
Recap string // shown on Review
CarryForward string // shown on Planning
}
- On Review, the view carries
Status+Recap(and theCarryForwardpreview). - On Planning, the view carries the
CarryForwardline.
Unlike the M6 profile (large and private, deliberately kept off the wire), the reflection lines are short and exist to be displayed, so they are intentionally included in the State payload sent to the browser.
Persistence
Snapshot-only, latest-wins. The persisted snapshot JSON gains reflectionRecap,
carryForward, and a small reflectionStatus enum (idle/pending/ready/absent).
There are no changes to audit.jsonl, no new files, and no new on-disk
format. The permanent, hash-chained SessionSummary is untouched.
One small additive reader is needed on the store:
// RecentSessions returns up to n most-recent summaries from the audit chain,
// most-recent first (or oldest-first — fixed by the plan), [] if the log is
// absent or empty.
func RecentSessions(path string, n int) ([]SessionSummary, error)
Today's readSummaries is unexported; RecentSessions exposes a bounded slice
of it for the controller to format into history.
UI (web static assets)
- Review screen: the
Recaprendered as a subtle line (nudge-band style), withpendingandabsentstates. The End button works immediately regardless of reflection status. - Planning screen: the
CarryForwardrendered as a quiet one-liner ("Last time: …"), mirroring the M6 knowledge indicator. No buttons, no added clicks anywhere.
Daemon wiring (cmd/antidriftd/main.go)
The reviewer backend is wired into the controller (ctrl.SetReviewer(...)),
gated on AI-backend availability exactly like the other roles. With no backend
configured, the reviewer is simply absent and reflection silently does nothing.
Error handling / graceful degradation
- Backend off, error, empty output, unparseable result, or no prior history → nothing is shown; Review, End, and Planning behave exactly as today.
- The reviewer never blocks a transition.
Enddoes not wait for it. - The generation counter discards late results from a superseded review.
Testing
ai: the Reviewer prompt includes bothfinishedandhistory; the parser extracts two lines; error/blank/unparseable input yields an emptyReflection.session: reflection is fetched onenterReview; the result is cached and rides the snapshot; a stale (superseded-generation) result is discarded; thecarryForwardcomposes into the next coach'sgrounding; everything degrades gracefully when no reviewer is set;RecentSessionsreturns the last n summaries in the expected order.web: the Review payload carries theRecap; the Planning payload carries theCarryForward; reflection text is intentionally present on the wire.
Out of scope (this milestone)
- A durable reflection history (
reflections.jsonl). Nothing in the loop needs it — the next coach only needs the latest carry-forward — and the permanentSessionSummarystill records outcome/buckets/switches for every session. Promoting to a durable log later would be an additive change. - Changing the
ai.Coachsignature. Grounding is already a free-form string. - Refactoring
session.go. It is ~1054 lines and M7 adds another per-role async-fetch block; the repetition across the coach/tasks/knowledge/reviewer fetchers is a fair future consolidation target, but extracting it now would destabilize four working roles for no functional gain. M7 follows the established per-role pattern for consistency and reviewability.