Skip to content

Goals HUD panel: sync completed goal eviction with viewport boundary #490

Description

@hoblin

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:

  1. set_goal — creation
  2. update_goal — zero or more description edits
  3. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions