What
When the Mneme boundary advances past every phantom pair belonging to a completed goal (set_goal / update_goal / finish_goal for that goal_id), the goal should disappear from the TUI HUD goals panel. Currently completed goals stay visible in the HUD until the session ends.
This is the goals counterpart to #431 — the same lifecycle gap that issue closed for sub-agents, applied to goals now that they live as phantom messages (#403) instead of in the system prompt.
Solution: flip evicted_at + reuse existing broadcast
The plumbing on the read path is already in place:
Goal.not_evicted scope filters evicted_at IS NULL (app/models/goal.rb:31)
Session#goals_summary already calls .not_evicted (app/models/session.rb:312)
Goal#broadcast_goals_update fires goals_updated on every commit (app/models/goal.rb:136)
So the moment evicted_at is set on a goal, the after_commit broadcast pushes a fresh summary that omits it and the TUI HUD removes it. What's missing is the eviction hook that actually flips the column.
Goal traces in the viewport
A completed goal leaves multiple tool_call/tool_response pairs in the message stream:
set_goal — creation
update_goal — zero or more description edits
finish_goal — completion
A goal's HUD entry should drop only when none of these phantom pairs survive above the Mneme boundary. The lookup mirrors Session#subagent_trace_in_viewport? (app/models/session.rb:510) — query messages above mneme_boundary_message_id whose tool_name matches one of the goal tools and whose payload references this goal_id.
Eviction hook
In Mneme::Runner (lib/mneme/runner.rb:108), the existing refresh_subagent_visibility runs after each boundary advance. Add a sibling refresh_goal_visibility (called from the same place) that iterates session.goals.evictable (already-defined scope: completed.where(evicted_at: nil)) and, for each goal with no surviving traces, sets evicted_at = Time.current. The after_commit :broadcast_goals_update callback handles the TUI sync for free — no new event class or broadcaster needed.
What already works
goals_updated broadcast on every Goal commit ✅
goals_summary filters evicted goals ✅
- TUI handler
handle_goals_updated consumes the broadcast (lib/tui/screens/chat.rb:472) ✅
Goal.evictable scope (completed + not yet evicted) ✅
What's missing
Session#goal_trace_in_viewport?(goal) — boundary-aware trace lookup ❌
Mneme::Runner#refresh_goal_visibility — boundary-advance hook that flips evicted_at ❌
Why reuse goals_updated instead of a new goal_evicted event?
Sub-agents needed a dedicated SubagentEvicted event because the hud_visible column has no after_commit broadcast — child.update_column is a column-level write that bypasses callbacks. Goals already have a per-commit broadcast wired up, so a normal update!(evicted_at: Time.current) triggers the existing pipeline. Adding a parallel event would be redundant.
Related
References
- Sub-agent design:
thoughts/shared/notes/2026-04-03/qa-brainstorm-event-loop-mailbox.md — "Sub-agent HUD Visibility Lifecycle" section
- Goal phantom-message design:
thoughts/shared/notes/2026-04-02/goals-to-phantom-messages.md (LLM-side only — this issue covers the TUI gap left open there)
What
When the Mneme boundary advances past every phantom pair belonging to a completed goal (
set_goal/update_goal/finish_goalfor thatgoal_id), the goal should disappear from the TUI HUD goals panel. Currently completed goals stay visible in the HUD until the session ends.This is the goals counterpart to #431 — the same lifecycle gap that issue closed for sub-agents, applied to goals now that they live as phantom messages (#403) instead of in the system prompt.
Solution: flip
evicted_at+ reuse existing broadcastThe plumbing on the read path is already in place:
Goal.not_evictedscope filtersevicted_at IS NULL(app/models/goal.rb:31)Session#goals_summaryalready calls.not_evicted(app/models/session.rb:312)Goal#broadcast_goals_updatefiresgoals_updatedon every commit (app/models/goal.rb:136)So the moment
evicted_atis set on a goal, the after_commit broadcast pushes a fresh summary that omits it and the TUI HUD removes it. What's missing is the eviction hook that actually flips the column.Goal traces in the viewport
A completed goal leaves multiple tool_call/tool_response pairs in the message stream:
set_goal— creationupdate_goal— zero or more description editsfinish_goal— completionA goal's HUD entry should drop only when none of these phantom pairs survive above the Mneme boundary. The lookup mirrors
Session#subagent_trace_in_viewport?(app/models/session.rb:510) — query messages abovemneme_boundary_message_idwhosetool_namematches one of the goal tools and whose payload references thisgoal_id.Eviction hook
In
Mneme::Runner(lib/mneme/runner.rb:108), the existingrefresh_subagent_visibilityruns after each boundary advance. Add a siblingrefresh_goal_visibility(called from the same place) that iteratessession.goals.evictable(already-defined scope:completed.where(evicted_at: nil)) and, for each goal with no surviving traces, setsevicted_at = Time.current. Theafter_commit :broadcast_goals_updatecallback handles the TUI sync for free — no new event class or broadcaster needed.What already works
goals_updatedbroadcast on every Goal commit ✅goals_summaryfilters evicted goals ✅handle_goals_updatedconsumes the broadcast (lib/tui/screens/chat.rb:472) ✅Goal.evictablescope (completed + not yet evicted) ✅What's missing
Session#goal_trace_in_viewport?(goal)— boundary-aware trace lookup ❌Mneme::Runner#refresh_goal_visibility— boundary-advance hook that flipsevicted_at❌Why reuse
goals_updatedinstead of a newgoal_evictedevent?Sub-agents needed a dedicated
SubagentEvictedevent because thehud_visiblecolumn has no after_commit broadcast —child.update_columnis a column-level write that bypasses callbacks. Goals already have a per-commit broadcast wired up, so a normalupdate!(evicted_at: Time.current)triggers the existing pipeline. Adding a parallel event would be redundant.Related
not_evictedfromgoals_summary(still load-bearing)References
thoughts/shared/notes/2026-04-03/qa-brainstorm-event-loop-mailbox.md— "Sub-agent HUD Visibility Lifecycle" sectionthoughts/shared/notes/2026-04-02/goals-to-phantom-messages.md(LLM-side only — this issue covers the TUI gap left open there)