Skip to content

feat(soul): surface the most recently active desire as current#250

Merged
oratis merged 3 commits into
mainfrom
claude/desire-current-pick
Jul 15, 2026
Merged

feat(soul): surface the most recently active desire as current#250
oratis merged 3 commits into
mainfrom
claude/desire-current-pick

Conversation

@oratis

@oratis oratis commented Jul 15, 2026

Copy link
Copy Markdown
Owner

PR3 of the desire-evolution plan (#242#249 → this). Supersedes #247, which GitHub auto-closed when its base branch claude/desire-reflect-evolve was deleted on #249's merge (can't reopen a closed PR whose base is gone). Same branch, rebased onto main, plus the review fix below.

What

  • pickCurrentDesire(desires, activityAt?) — pure. Prefers actionable; within that pool most-recently-active wins; falls back to most-recently-born. Order-independent, stable between events.
  • desireActivity(desires)fs.stat over each <slug>.md + <slug>.progress.md, so "activity" tracks both authoring (add/revise) and pursuit (heartbeat progress).
  • Wired into GET /api/island/ping (room + island + web client all consume it) and marked ← current in lisa status.

Review fixes folded in

  • desireActivity no longer rejects on an unsafe slug (a stray ._x.md AppleDouble in the git-synced soul dir would abort lisa status) — path construction moved inside the try; regression test added.
  • pickCurrentDesire given a deterministic slug secondary key so an exact recency tie doesn't fall back to fs.readdir order.

Reviewed + tested: pick-desire.test.ts + desire-activity.test.ts green, typecheck clean.

🤖 Generated with Claude Code

oratis and others added 3 commits July 15, 2026 11:04
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>
…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>
@oratis
oratis merged commit 3514c30 into main Jul 15, 2026
1 check passed
@oratis
oratis deleted the claude/desire-current-pick branch July 15, 2026 03:09
oratis added a commit that referenced this pull request Jul 15, 2026
…cus gate (#251)

From the #248/#249 reviews (the two MED items not already handled by #250's
review fix, which added the withSoulLock + desireActivity guard):

- Persisted CLOSED marker (#249): DesireEntry gains `closed`; writeDesire
  serializes `closed: yes` and now keeps heartbeatPrompt/pursuit even when not
  actionable, so closing preserves them for a clean re-open. closeDesire flips
  closed on (+ actionable off) and is idempotent — an already-closed desire is a
  no-op, so it can't append duplicate [DESIRE_CLOSED]/[CLOSED] notes. reviseDesire
  clears closed when it re-opens (actionable:true). Closed desires are filtered
  out of the reflector's "revise or close" block (so the list actually shrinks)
  and never surface as her current/focused desire (ping + `lisa status`).

- Restart-safe focus freshness (#248): the intra-session focus gate keyed on
  reflectClock.idleFor(), which reads "fresh" right after a launchd restart and
  would pin focus onto a stale resumed conversation. Gate on a new
  lastUserMessageAt (0 until a real POST /chat arrives, so a restart closes the
  gate until the user actually talks).

Adds closed-marker/idempotency/re-open tests. Full suite green (1013 pass).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant