What is a Lesson
ALesson is a four-line record:
memory.Fact, Content becomes:
lesson and tags include reflexion + trigger:<x> + domain-specific tags. This enables precise queries: “show me all lessons about edit-file” becomes a regular memory search.
Four triggers
- OnError
- OnHallucination
- OnLowQuality
- Manual via /reflect
Error != nil. Examples: timeout, invalid tool call, provider crash.
Default: ON.Flow — durable mode (default)
WithCHATCLI_QUALITY_REFLEXION_QUEUE_ENABLED=true (default), triggers flow through a persistent queue. The hook never blocks the turn and the process can crash without losing the lesson:
PostRun inspects the trigger
ReflexionHook.PostRun(ctx, hc, result) looks at result.Metadata + result.Error — if no gate matches, returns in μs.WAL Append (synchronous, sub-ms)
enqueuer.Enqueue(req). The Runner computes JobID = sha256(task|trigger|attempt)[:16], writes a record to the WAL (~/.chatcli/reflexion/wal/<id>.wal) via tmp → fsync → atomic rename → dir fsync, then pushes in-memory.Immediate return to the pipeline
Worker pool processes async
GenerateLesson with per-job timeout (default 2 min), and persists to memory.Fact unless the LLM emits <skip>.Outcome classification
Replay on boot
Runner.Replay() runs async and re-queues every pending record from the WAL (discarding those older than StaleAfter, default 7 days).Fallback: legacy mode (detached goroutine)
IfCHATCLI_QUALITY_REFLEXION_QUEUE_ENABLED=false, the hook reverts to the original behavior:
Durable Queue — WAL + Worker Pool + DLQ
The queue is implemented incli/agent/quality/lessonq/ with enterprise guarantees:
WAL (Write-Ahead Log)
Each pending lesson is a.wal file in ~/.chatcli/reflexion/wal/ — one per Job ID. Binary layout:
- Double CRC detects torn writes (crash mid-fsync). Corrupt records are discarded on replay +
chatcli_lessonq_wal_corruption_totalincrements. - Atomic rename: write to
<id>.tmp.<pid>.<seq>→ fsync → rename → dir fsync. A reader never sees a partial record. - O(1) ACK: a single
unlinkremoves the record. No background compaction.
Worker Pool
- Blocking Dequeue (waits until NextAttemptAt ≤ now).
- Bounded per-job timeout (doesn’t inherit turn ctx — reflexion outlives the turn by design).
- Panic recovery: if the processor panics, goes straight to DLQ (retrying a bug loops).
- Emits
chatcli_lessonq_processing_duration_seconds{outcome}.
Dead Letter Queue
Permanent failures or retry exhaustion go to~/.chatcli/reflexion/dlq/ (same WAL format, read-only to the process). Operator inspects and decides:
Retry with Jitter
Transient errors (ctx timeout, provider 429/503, temp fs error) become reschedules:Idempotency
JobID is content-addressed: sha256(normalized(task) | trigger | attempt | outcome)[:16]. Re-triggering the same situation while the job is in-flight is a no-op (WAL exists → Runner skips queue insert). Whitespace is normalized to avoid inflation from trivial churn.
Drain + Graceful Shutdown
On exit (cli.cleanup()), the Runner enters DrainAndShutdown(30s):
- Queue closes — no new dequeues.
- Workers finish in-flight (or get cancelled on timeout).
- WAL/DLQ close.
/reflect — Commands
/reflect retry and /reflect purge list live DLQ IDs with task preview + last error.Files and layout
CHATCLI_QUALITY_REFLEXION_QUEUE_BASE_DIR (default: <workspace>/.chatcli/reflexion).
Lesson generator protocol
The system prompt instructs the model to be general, not one-off:/reflect — manual path without LLM
When you know the lesson and don’t need an LLM distilling:
memory.Fact:
["reflexion", "trigger:manual", "user-supplied"].
How the lesson “comes back”
Once persisted, the lesson is a regular fact in the index. It surfaces via:- Hint-based retrieval: if the next task mentions keywords in
Tags, the relevance-based scorer surfaces it. - HyDE amplifies: with
CHATCLI_QUALITY_HYDE_ENABLED=true, the generated hypothesis covers similar concepts, increasing match chance. - Vector search: with embeddings configured, the lesson is searched by cosine proximity.
## Long-term Memory section with the lesson text, and the model has all the cues to not repeat the mistake.
Environment variables
Gates (when to fire)
Durable queue (WAL + worker pool + DLQ)
Prometheus metrics
The queue emits 10 metrics underchatcli_lessonq_*:
Full cycle example
User asks for a task that fails
/coder refactor pkg/engine to extract Close methodCoderAgent tries full rewrite
PostRun detects result.Error != nil
goroutine: GenerateLesson
Persists in memory.Fact
lesson, workspace=current project.Next week, user asks for a similar refactor
/coder refactor pkg/auth/manager.go split into smaller filesRAG+HyDE brings the lesson
refactor + large-file match. Lesson appears in the system prompt.Coder picks the right approach from the start
@coder patch instead of write. Task done without timeout.Inspect stored lessons
Useful Prometheus snapshots
Legacy inspection (pre-queue)
See also
#4 RAG + HyDE
#6 CoVe
verified_with_discrepancy signal that Reflexion consumes.Bootstrap Memory
Memory Commands
/memory load, /memory show, /memory longterm.