feat(soul): surface the most recently active desire as current#247
feat(soul): surface the most recently active desire as current#247oratis wants to merge 2 commits into
Conversation
14a1ee2 to
3e0a73e
Compare
873aa85 to
21f848f
Compare
3e0a73e to
b902534
Compare
The "current desire" shown in the room/island ticker (and now `status`) was `desires.find(d => d.actionable)` over fs.readdir order — an arbitrary, static pick decoupled from recency and relevance. So even once desires started changing, the surfaced one didn't. Add two store helpers: - pickCurrentDesire(desires, activityAt?) — pure. Prefers actionable desires; within the pool, most-recently-active wins; falls back to most-recently-born. Because the key is a stored timestamp, the pick is stable between real events and only moves when something actually happens (add/revise/pursue/close) — fidelity, not per-request flicker. - desireActivity(desires) — cheap fs.stat over each <slug>.md and <slug>.progress.md, so activity reflects both authoring and pursuit. Wire into GET /api/island/ping (which the room, island, and web client all read, so all three update at once) and mark the pick in `lisa status`. PR 3 of 3 from docs/PLAN_DESIRE_EVOLUTION_v1.0.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pick-desire.test.ts covers the pure pickCurrentDesire; this covers the I/O half — desireActivity reading real desire/progress file mtimes off disk (pinned via fs.utimes) and feeding pickCurrentDesire end-to-end, including the pursuit-refreshes-an-old-desire case and the bornAt floor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
21f848f to
458f2e9
Compare
b902534 to
b292ef0
Compare
Review — one robustness fix before merge
|
…tie-break Per review of #247: desireFile()/desireProgressFile() run assertSafeSlug, which throws on an unsafe slug, but listDesires() derives slugs from raw filenames without validating. A stray file (e.g. a macOS `._x.md` AppleDouble in the git-synced soul dir) made desireActivity reject — which /api/island/ping caught (→ null) but `lisa status` did NOT, aborting the command. Move the path construction inside the try so such a desire is skipped, flooring at bornAt. Also give pickCurrentDesire a deterministic secondary sort key (slug) so an exact recency tie no longer falls back to fs.readdir order — the accident the PR removes. Adds an unsafe-slug regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR3 of the desire-evolution plan (supersedes auto-closed #247). pickCurrentDesire prefers actionable then most-recently-active (stable, order-independent); desireActivity stats desire+progress files; wired into /api/island/ping + 'lisa status'. Review fixes: unsafe-slug tolerance in desireActivity, deterministic tie-break.
…ocus From the PR reviews of #247/#248: - desireActivity (#247): build the desire file paths INSIDE the try so a pathological slug (leading dot / >64 chars) whose assertSafeSlug throws falls back to bornAt instead of crashing `lisa status` (the ping path already caught this; runStatus didn't). - pickFocusedDesire (#248): only consider actionable desires, mirroring pickCurrentDesire, so a lexically-matching CLOSED/dormant desire can't be surfaced as the conversation's focus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Why
The "current desire" ticker (room/island, and the web client) reads
current_desirefromGET /api/island/ping, which computed it asdesires.find(d => d.actionable)overfs.readdirorder — an arbitrary, static pick decoupled from recency and relevance. So even once desires start changing (PR #242 / #249), the surfaced one still wouldn't move (root cause #1 in the plan doc).What
Two store helpers:
pickCurrentDesire(desires, activityAt?)— pure. Prefers actionable desires (what's actually driving the heartbeat); within that pool the most-recently-active wins; falls back to most-recently-born. Because the ordering key is a stored timestamp, the pick is stable between real events and only moves when something actually happens — add / revise / pursue / close. That's fidelity, not per-request flicker.desireActivity(desires)— cheapfs.statover each<slug>.mdand<slug>.progress.md, so "activity" reflects both authoring (add/revise rewrite the desire file) and pursuit (the heartbeat appends the progress file).Wired into
GET /api/island/ping— which the room, island, and web client all consume, so all three update from one change — and marked (← current) inlisa statusso the CLI and UI agree.Design rationale (正反方辩论)
Plan doc Debate 3 weighs recency sort (feels alive) vs stable pick (steady north-star). Decision: sort by activity — the reframing that dissolves the tension is that a timestamp-driven key is stable between events, so you get the north-star feel and the responsiveness, which the readdir accident gave neither of. Full debate in
docs/PLAN_DESIRE_EVOLUTION_v1.0.md§4 (landed in #242).Testing
src/soul/pick-desire.test.ts— 7 tests: empty→null, actionable preference, activity-recency ordering, independence from array/readdir order, bornAt fallback, pursuit refreshing an old desire, all-dormant fallback.npm test: zero regressions — the 3 failures present (mail/scheduler,mail/servicefrom the missingimapflowoptional dep;web/auth) fail identically onmainwith and without this change (verified by stash-and-compare); +7 new passing tests. Typecheck clean.Merge order
Independent of #242 and #249 (touches disjoint regions of
store.ts/server.ts), but the series reads best merged #242 → #249 → this.🤖 Generated with Claude Code