fix(web): reflect on idle so web conversations evolve Lisa's desires#242
Merged
Conversation
This was referenced Jul 14, 2026
The web server never exits, so end-of-session reflection had no trigger: reflectOnSession was only wired to CLI --reflect and the channel router, and the POST /reflect endpoint had no caller. Result: desires never changed from web conversations — the "wish never changes" bug. Add a dedicated, debounced reflection scheduler in startWebServer. After 5 min of quiet (LISA_REFLECT_DEBOUNCE_MS) it reflects once on the accumulated history, provided the user actually added a new message since the last reflection. Counting user messages (not total) keeps Lisa's own idle "[while you were away]" notes from triggering a reflection on her own monologue. It reuses the process-wide idle watcher purely as an activity clock, is independent of the 60-min "dream" idle (works with --no-idle), advances its marker only on success (a failed reflect retries), and unref()s its timer so it never keeps the process alive. Also lands docs/PLAN_DESIRE_EVOLUTION_v1.0.md — the full plan (root-cause analysis, pro/con debates, PR1-3 breakdown) that this is the first PR of. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oratis
force-pushed
the
claude/desire-web-reflect
branch
from
July 14, 2026 17:23
13ed3ce to
4cf33fa
Compare
Owner
Author
Review — merge-ready ✅Walked the concurrency + debounce truth table against the code:
Sound — no blockers. Two small optional hardenings I'll fold in while merging:
Neither blocks the fix; merging after those. |
…ve reflect Two low-severity hardenings on the reflection scheduler: - Clamp LISA_REFLECT_DEBOUNCE_MS to a positive finite value. `Number(env) || DEFAULT` rejects NaN/""/"0" but a negative value is truthy → debounceMs<0 makes `idleMs < debounceMs` always false, reflecting ~60s into a live conversation. Now `Number.isFinite(n) && n > 0 ? n : DEFAULT`. - The scheduler's `inFlight: reflecting || idleRunning` already blocks reflect-during-dream, but the dream's own guard was only `if (idleRunning)`, so a dream could start mid-reflect and its "[while you were away]" note could bleed into the reflected history (PLAN §3 wants reflect first). Hoist `reflecting` up beside `idleRunning` and guard the dream on both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 15, 2026
oratis
added a commit
that referenced
this pull request
Jul 15, 2026
Per review of #249: wrap reviseDesire and closeDesire's read-modify-write of the desire file in withSoulLock, matching the other soul RMW paths — now that web idle-reflect (#242) can race a CLI reflect on the same slug, an unlocked list→write loses one field edit (last-writer-wins). writeDesire doesn't self-lock so this doesn't nest; closeDesire's progress/journal appends self-lock, so they stay outside the block (nesting would deadlock the non-reentrant file lock). Also fix the misleading comment: writeDesire only serializes heartbeatPrompt/pursuit while actionable, so closing drops them from disk (git history retains the last actionable version for re-open). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Investigating "the wish never changes no matter what we talk about" traced to a dead code path:
reflectOnSession— the only automatic way a conversation turns into or updates a desire — is never triggered in the web deployment. It's wired to CLI--reflectand the channel router only; thePOST /reflectendpoint exists but no client calls it. So web conversations never touched Lisa's desires. This is the dominant root cause (full analysis in the plan doc).What
A dedicated, debounced reflection scheduler in
startWebServer:LISA_REFLECT_DEBOUNCE_MS(default 5 min) of no user input, reflect once on the accumulated history.[while you were away]notes never trigger reflection on her own monologue.src/cli.tsdefault) — works even with--no-idle. It reuses the process-wide idle watcher purely as an activity clock (/chatalreadytick()s it).unref()s its timer; skips while a reflection or idle run is in flight.reflect_doneevent and appends to the session log for observability.Decision logic is extracted to a pure, unit-tested module (
src/web/reflect-scheduler.ts).Plan doc
This is PR 1 of 3 from
docs/PLAN_DESIRE_EVOLUTION_v1.0.md, which bundles the root-cause analysis, the pro/con debates (正反方辩论), and the PR1–3 breakdown. PR2 makes reflection able to revise/close desires (not just append); PR3 surfaces the most recently active desire instead of a filesystem-order pick.Testing
src/web/reflect-scheduler.test.ts— 8 tests covering the debounce/guard truth table and user-message counting. All green.npm run typecheck: no new errors (pre-existingimapflowoptional-dep error only).npm test: zero regressions — same 5 pre-existing failures asmain(imapflow mail tests, a stale HTML snapshot, an auth test), plus the 8 new passing tests.Backend timer change — not observable in the browser preview; verified via unit tests + typecheck.
🤖 Generated with Claude Code