From ec1c7a8266df8696dbc60c50f1a592c5a1d22af3 Mon Sep 17 00:00:00 2001
From: Zachary Lowden
Date: Sat, 31 Oct 2026 23:32:03 -0500
Subject: [PATCH 1/7] fix(attention): asks and waiting sessions name the exact
tmux target
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
An ask, a waiting session and their notifications named a mirrored window
by its host's codename ("mango:1 @ ws", which is no tmux name) or by its tmux
session alone with no window ("build @ ws"), so the operator could not find
it. Every attention surface that names a mirrored window now takes its words
from one helper, mirror.Where: "tmux : @ ", led by the
codename when the host has one ("mango:1 · tmux scratch3:5 @ ws").
- chiefnudge.Session carries the window's Index (filled by stallInputs), and
nudgeAskQuestion builds its "where" from mirror.Where; agents keep
"(agent )".
- A waiting session / held question / Hermes item's source is e.Where().
- A mirrored session's ask resolves the window running its Claude session
NOW (claudeSessionEntry, the rule claudeSessionWindow already used) for its
source on the queue, its page header, its session page's asks block and
its notification; when no window runs it, its stored name is followed by
"(not shown in any mirrored window now)" instead of an invented target.
- Docs: attention.md "How a window is named", chief-nudges.md ask text.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
docs/attention.md | 32 ++-
docs/chief-nudges.md | 8 +-
internal/api/attention.go | 57 +++++-
internal/api/attention_ask_boxes_test.go | 6 +-
internal/api/attention_ask_page.go | 2 +-
internal/api/attention_asker.go | 2 +-
internal/api/attention_push_target.go | 2 +-
internal/api/attention_push_target_test.go | 10 +-
internal/api/attention_suggest.go | 5 +-
internal/api/attention_test.go | 4 +-
internal/api/attention_ux_pg_test.go | 4 +-
internal/api/attention_where_test.go | 219 +++++++++++++++++++++
internal/api/cards_attention.go | 14 +-
internal/api/chief_nudge.go | 8 +-
internal/api/chief_nudge_pg_test.go | 2 +-
internal/api/unrouted_push_test.go | 6 +-
internal/chiefnudge/chiefnudge.go | 4 +
internal/mirror/order.go | 51 ++++-
internal/mirror/where_test.go | 31 +++
19 files changed, 423 insertions(+), 44 deletions(-)
create mode 100644 internal/api/attention_where_test.go
create mode 100644 internal/mirror/where_test.go
diff --git a/docs/attention.md b/docs/attention.md
index 5738ff13..19086677 100644
--- a/docs/attention.md
+++ b/docs/attention.md
@@ -14,7 +14,7 @@ creation), ties broken by the item's key:
| kind | what | where it comes from | how you act on it |
|---|---|---|---|
| Question | An agent called `attention_ask`, or muster raised one itself, shown as asked by `muster`: a plugin's mention of an in-progress task when you have no chief ([task routing](plugins.md#routing-a-task-to-the-chief)), or an agent-mail message your chief did not act on ([plugins](plugins.md#a-message-the-chief-does-not-act-on)); nothing waits on its answer. | `attention_asks` (migration 0025; muster's own asker, 0028; the message link, 0051) | Tap one of the agent's options, or type an answer. **Dismiss without answering** withdraws it. |
-| Session waiting | A mirrored Claude Code **or Hermes** window whose session reports `waiting` (a permission prompt, a question; for Hermes an approval, a clarify question or a sudo password). Shown with **what it waits on** (below), its focus title, its window @ host and its age. | The mirror hub's live window list; nothing is stored. | **Open the session** (the rendered view). A Hermes **approval** on a host with `-hermes-answer` also offers **Allow once** / **Deny**, answered through Hermes's gateway ([mirroring.md](mirroring.md)). It leaves the list when the session stops waiting. |
+| Session waiting | A mirrored Claude Code **or Hermes** window whose session reports `waiting` (a permission prompt, a question; for Hermes an approval, a clarify question or a sudo password). Shown with **what it waits on** (below), its focus title, its exact tmux target ([``](#how-a-window-is-named)) and its age. | The mirror hub's live window list; nothing is stored. | **Open the session** (the rendered view). A Hermes **approval** on a host with `-hermes-answer` also offers **Allow once** / **Deny**, answered through Hermes's gateway ([mirroring.md](mirroring.md)). It leaves the list when the session stops waiting. |
| Session question | A mirrored session's `AskUserQuestion`, registered by the `muster-mirror ask` hook and shown in the terminal and the rendered view at once. | The open question (`internal/askq`), live; it leaves the queue once answered in either place, dismissed, replaced by a newer question, or its session is gone. Such a window is this ONE item — its question, or, when it asks several, each one's first line, numbered — never also "Session waiting". | **Answer in the session**: the rendered view, where the question card is. |
| Task to route | A plugin handed a task to your chief ([task routing](plugins.md#routing-a-task-to-the-chief)) and the chief has not routed it: there is no chief, it is not running or busy, or it took the event and did not act. Shown as the task's number and title. | `plugin_messages` task events with no routing or start recorded for their task at or after the event's latest delivery attempt, or its creation if it has had none (audit rows), whose task is not complete or dismissed; nothing is copied. | **Open the task**. It leaves the list when the chief routes the task or starts an agent on it, or the task is complete or dismissed. No notification of its own: the task's "New task" push already went. |
| Reply draft | Your chief drafted a reply to a plugin message. Shown as the message's sender and its first 160 characters, and the draft reply's first 160. | `plugin_reply_drafts`, pending ones; nothing is copied. | Not on the Ask tab: with [plugins](plugins.md) on, the page has a second tab, **Reply drafts** (below), whose label carries the count ("Reply drafts (2)"). The draft is still in the badge's count, its push and `attention_list`. |
@@ -108,8 +108,34 @@ command, path or URL, never what Claude said.
| item | title | body |
|---|---|---|
| a question (an agent's, the chief's, muster's own) | ` is asking` | the question, on one line, then `Options: a · b · c` when it offered any — except muster's own ask about a [plugin message nobody acted on](plugins.md#a-message-the-chief-does-not-act-on), whose long fixed sentence would fill the body: its body is `From: · Subject: `, then the option (the page shows the whole question) |
-| a held question (a session's `AskUserQuestion`) | ` @ is asking` | the question (each one's first line, numbered, when it asks several), then one question's choices (in the push only: `attention_list` carries no `options` for it, as it is answered on its question card, not with `attention_answer`) |
-| a waiting session | `A session is waiting on you` | `Waiting: · @ `, or `Waiting: @ ` when its host names no tool |
+| a held question (a session's `AskUserQuestion`) | ` is asking` | the question (each one's first line, numbered, when it asks several), then one question's choices (in the push only: `attention_list` carries no `options` for it, as it is answered on its question card, not with `attention_answer`) |
+| a waiting session | `A session is waiting on you` | `Waiting: · `, or `Waiting: ` when its host names no tool |
+
+`` is the window's exact tmux target — see [How a window is named](#how-a-window-is-named).
+
+### How a window is named
+
+Every attention text that names a mirrored window — a chief nudge's ask, a
+waiting session, a session question, a mirrored session's own ask (its source
+on the queue, its page's "** asks" header and the asks block of its
+session page), and their notifications — names it the same way
+(`mirror.Where`), so you can go straight to it in a terminal:
+
+- `tmux : @ ` — the tmux session name and window
+ index, exactly as tmux's own target syntax takes them
+ (`tmux build:4 @ laptop`);
+- led by the host's codename for the window when it has one
+ (`mango:1 · tmux scratch3:5 @ laptop`). The codename is a display name,
+ not a tmux name, so it is never the only one.
+
+The index is a snapshot: tmux renumbers windows, so the text says where the
+window was when it was written (a chief nudge's ask, when it was raised; the
+queue, its page and a notification, when they were drawn or sent). The **View
+in session** link finds the window by its Claude session id when you follow
+it, and is the source of truth. A mirrored session's ask whose Claude session
+no window shows now reads as its stored name and host followed by
+`(not shown in any mirrored window now)` — muster names no target it cannot
+see. An agent's ask is named by the agent.
Each line is folded to one line, anything shaped like a secret is redacted, and
the body is cut to 200 characters on a character boundary with a visible `…`
diff --git a/docs/chief-nudges.md b/docs/chief-nudges.md
index ef148600..5605589a 100644
--- a/docs/chief-nudges.md
+++ b/docs/chief-nudges.md
@@ -362,7 +362,13 @@ chief judged; if it cannot read the session's state, the nudge stays queued and
is tried again.
**An ask** is an attention ask on your queue, asked by *your chief*, naming the
-session's focus title and where it runs, the chief's question and its options.
+session's focus title and where it runs, the chief's question and its options:
+"** (**) stopped and is waiting on you: …", where **
+is a mirrored window's exact tmux target as of when the ask was raised
+(`Fix CI (tmux build:4 @ laptop)`, or
+`(mango:1 · tmux scratch3:5 @ laptop)` when its host gives the window a
+codename — [attention.md](attention.md#how-a-window-is-named)), and an agent's
+`(agent )`.
It is pushed like any ask, and its page and its card on the Asks list link the
session it is about (**View in session**: the window now running that Claude
session, or the agent's page at the conversation that stalled), found through
diff --git a/internal/api/attention.go b/internal/api/attention.go
index d7512e75..54e38d50 100644
--- a/internal/api/attention.go
+++ b/internal/api/attention.go
@@ -261,7 +261,10 @@ func sessionKey(host, session, window string) string {
return "session-" + hex.EncodeToString(sum[:8])
}
-// askerLabel is what the view shows as an ask's source.
+// askerLabel is an ask's source by what it stored: its label (for a mirrored
+// session, its name and host when it asked — no tmux target), else its ids.
+// What an OPEN ask shows as its source is askSource; this is the answered
+// list's and its delivery line's, which send the operator nowhere.
func askerLabel(a attention.Ask) string {
if a.Asker.Label != "" {
return a.Asker.Label
@@ -272,8 +275,46 @@ func askerLabel(a attention.Ask) string {
return a.Asker.MirrorSession + " @ " + a.Asker.MirrorHost
}
-func askItem(a attention.Ask) attentionItem {
- it := attentionItem{Key: attentionAskKey(a.ID), Kind: attentionKindAsk, Source: askerLabel(a), Text: a.Question,
+// askNotShown follows an asking mirrored session's stored name when no
+// window lists its Claude session now: muster says so rather than name a tmux
+// target it cannot vouch for.
+const askNotShown = " (not shown in any mirrored window now)"
+
+// askSource is what an ask names as its source: for a mirrored session, the
+// window running its Claude session NOW, by its exact tmux target
+// (mirror.Where, resolved through claudeSessionEntry over hosts) — or its
+// stored name and askNotShown when no window does; for every other asker (an
+// agent, muster itself) askerLabel.
+func askSource(a attention.Ask, hosts []mirror.HostView) string {
+ if a.Asker.MirrorSession == "" || a.Asker.AgentID > 0 || a.Asker.System {
+ return askerLabel(a)
+ }
+ if e, ok := claudeSessionEntry(hosts, a.Asker.MirrorHost, a.Asker.MirrorSession); ok {
+ return e.Where()
+ }
+ return askerLabel(a) + askNotShown
+}
+
+// askNeedsHosts reports whether askItem needs the owner's windows for a
+// (askSource): only a mirrored session's ask does.
+func askNeedsHosts(a attention.Ask) bool {
+ return a.Asker.MirrorSession != "" && a.Asker.AgentID == 0 && !a.Asker.System
+}
+
+// askItemNow is askItem over ctx's principal's windows, read only when a
+// needs them (askNeedsHosts).
+func (s *Server) askItemNow(ctx context.Context, a attention.Ask) attentionItem {
+ var hosts []mirror.HostView
+ if askNeedsHosts(a) && s.ext.Mirror != nil {
+ hosts = s.ext.Mirror.Hosts(ctx)
+ }
+ return askItem(a, hosts)
+}
+
+// askItem is ask a as an attention item; hosts is its owner's windows, which
+// a mirrored session's ask names its source from (askSource).
+func askItem(a attention.Ask, hosts []mirror.HostView) attentionItem {
+ it := attentionItem{Key: attentionAskKey(a.ID), Kind: attentionKindAsk, Source: askSource(a, hosts), Text: a.Question,
Options: a.Options, Since: a.CreatedAt, Link: attentionPath + "#" + attentionAskKey(a.ID), AskID: a.ID, suggestions: a.Suggestions}
if a.Asker.System {
it.systemAsk = true
@@ -388,7 +429,7 @@ func (s *Server) waitingSessions(ctx context.Context) []attentionItem {
continue
}
ref := ui.MirrorRef{Host: e.Host, Session: e.Session, Window: e.Window}
- it := attentionItem{Key: k, Kind: attentionKindSession, Source: e.DisplayTitle() + " @ " + e.Host, Text: sessionWaitingText, host: e.Host,
+ it := attentionItem{Key: k, Kind: attentionKindSession, Source: e.Where(), Text: sessionWaitingText, host: e.Host,
claudeSession: e.ClaudeSession, Offline: wt.offline, Link: ref.ViewPath(), card: attentionCard{ref: ref}}
if e.ClaudeSession != "" {
it.subject = focus.MirrorSubject(e.ClaudeSession)
@@ -528,13 +569,19 @@ func (s *Server) attentionItems(ctx context.Context) ([]attentionItem, error) {
if err != nil {
errs = append(errs, fmt.Errorf("asks: %w", err))
}
+ // The owner's windows, read once and only when a mirrored ask needs them.
+ var hosts []mirror.HostView
+ listed := false
for _, a := range asks {
// A question to the chief is the chief's (chief_ask_tool.go), not
// the owner's: the chief asks the owner with attention_ask if it must.
if a.ToChief {
continue
}
- out = append(out, askItem(a))
+ if askNeedsHosts(a) && !listed && s.ext.Mirror != nil {
+ hosts, listed = s.ext.Mirror.Hosts(ctx), true
+ }
+ out = append(out, askItem(a, hosts))
}
}
out = append(out, s.waitingSessions(ctx)...)
diff --git a/internal/api/attention_ask_boxes_test.go b/internal/api/attention_ask_boxes_test.go
index b5cae29e..0ebf8d3c 100644
--- a/internal/api/attention_ask_boxes_test.go
+++ b/internal/api/attention_ask_boxes_test.go
@@ -76,7 +76,7 @@ func boxServer(t *testing.T, dictate bool) *Server {
}
func systemAsk(id int64) attentionItem {
- return askItem(attention.Ask{ID: id, Owner: 1, Asker: attention.Asker{System: true, Label: chiefNudgeAskLabel}, Question: "q", CreatedAt: time.Now()})
+ return askItem(attention.Ask{ID: id, Owner: 1, Asker: attention.Asker{System: true, Label: chiefNudgeAskLabel}, Question: "q", CreatedAt: time.Now()}, nil)
}
// TestAChiefNudgesAskLinksTheSessionItIsAbout: a nudge's ask is muster's own
@@ -148,8 +148,8 @@ var dictateTag = regexp.MustCompile(`
`,
- `data-attention-focus>Publish the CLI release`, `data-attention-source>main:1 @ ws`} {
+ `data-attention-focus>Publish the CLI release`, `data-attention-source>tmux main:1 @ ws`} {
if !strings.Contains(page, want) {
t.Fatalf("the page lacks %q: %s", want, page)
}
@@ -180,7 +180,7 @@ func TestAWaitingSessionShowsWhatItWaitsOn(t *testing.T) {
// The notification says who and where, and the tool's NAME — never the
// command or Claude's words (operator decision: a waiting session stays
// summary only; the page has them).
- if body != "Waiting: Bash · main:1 @ ws" {
+ if body != "Waiting: Bash · tmux main:1 @ ws" {
t.Fatalf("the push body: %q", body)
}
}
diff --git a/internal/api/attention_where_test.go b/internal/api/attention_where_test.go
new file mode 100644
index 00000000..1327f20d
--- /dev/null
+++ b/internal/api/attention_where_test.go
@@ -0,0 +1,219 @@
+package api
+
+import (
+ "context"
+ "html"
+ "io"
+ "log"
+ "net/http/httptest"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/ZacxDev/muster/internal/attention"
+ "github.com/ZacxDev/muster/internal/chiefnudge"
+ "github.com/ZacxDev/muster/internal/mirror"
+ "github.com/ZacxDev/muster/internal/push"
+ "github.com/ZacxDev/muster/internal/ui"
+)
+
+// WHERE AN ATTENTION TEXT SENDS THE OPERATOR: every surface that names a
+// mirrored window names its exact tmux target, ":" on its
+// host, led by the host's codename when it has one (mirror.Where). Before,
+// they named the codename alone ("mango:1 @ ws", which is no tmux name) or the
+// session alone with no window ("build @ ws").
+//
+// The fixture's fields are pairwise distinct so a target built from the wrong
+// one cannot pass: the labelled window is tmux session "scratch3", window id
+// "@37", index 5, codename "mango:1" (its codename's index is NOT its tmux
+// index); the unlabelled one is session "build", id "@4", index 7.
+const (
+ whereLabelledSID = "5a1b2c3d-1111-4111-8111-000000000037"
+ whereUnlabelledSID = "5a1b2c3d-2222-4222-8222-000000000004"
+ whereGoneSID = "5a1b2c3d-3333-4333-8333-000000000099"
+)
+
+func whereServer(t *testing.T, asks ...attention.Ask) *Server {
+ t.Helper()
+ s := New(nil, AuthConfig{}, log.New(io.Discard, "", 0))
+ fm := &fakeMirror{online: true, interactive: true, statusReport: true, v2: &fakeMirrorV2{}, windows: []mirror.Window{
+ {Session: "scratch3", ID: "@37", Index: 5, Active: true, Label: "mango:1", Claude: true, ClaudeSession: whereLabelledSID, Status: mirror.StatusWaiting, StatusSince: attnSince},
+ {Session: "build", ID: "@4", Index: 7, Active: true, Claude: true, ClaudeSession: whereUnlabelledSID, Status: mirror.StatusWaiting, StatusSince: attnSince},
+ }}
+ ext := Extensions{Users: stubUsers{}, Mirror: fm}
+ if asks != nil {
+ ext.Attention = openAsks{asks: asks}
+ }
+ s.UseExtensions(ext)
+ return s
+}
+
+// whereSessionItem is the waiting-session item of window id.
+func whereSessionItem(t *testing.T, s *Server, session, window string) attentionItem {
+ t.Helper()
+ for _, it := range s.waitingSessions(s.attentionPrincipal(context.Background(), 1)) {
+ if it.Key == sessionKey("ws", session, window) {
+ return it
+ }
+ }
+ t.Fatalf("window %s:%s is not waiting", session, window)
+ return attentionItem{}
+}
+
+// TestANudgeAskNamesItsWindowsExactTmuxTarget: a chief nudge's ask, built from
+// the sweep's own candidate (stallInputs carries the window), names the tmux
+// session and index — with the codename first when the host gave one.
+func TestANudgeAskNamesItsWindowsExactTmuxTarget(t *testing.T) {
+ s := whereServer(t)
+ byKey := map[string]chiefnudge.Session{}
+ for _, c := range s.stallInputs(s.attentionPrincipal(context.Background(), 1), nil) {
+ byKey[c.Session.Key] = c.Session
+ }
+ if len(byKey) != 2 {
+ t.Fatalf("fixture: %d candidates, want 2", len(byKey))
+ }
+ for _, c := range []struct {
+ name, sid, want string
+ }{
+ {"a window with a codename", whereLabelledSID, "Fix CI (mango:1 · tmux scratch3:5 @ ws) stopped and is waiting on you:\n\nMerge now?\n\nYour answer is sent into that session word for word."},
+ {"a window without a codename", whereUnlabelledSID, "Fix CI (tmux build:7 @ ws) stopped and is waiting on you:\n\nMerge now?\n\nYour answer is sent into that session word for word."},
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ sess := byKey[c.sid]
+ sess.Title = "Fix CI"
+ if got := nudgeAskQuestion(sess, "Merge now?", false); got != c.want {
+ t.Fatalf("ask text:\n%q\nwant:\n%q", got, c.want)
+ }
+ })
+ }
+}
+
+// TestAnAgentNudgeAskKeepsItsAgentName: an agent has no tmux window; its ask
+// still names it "(agent )". An INVARIANT guard (it held before too).
+func TestAnAgentNudgeAskKeepsItsAgentName(t *testing.T) {
+ c := chiefnudge.Session{Kind: chiefnudge.KindAgent, Label: "Worker Seven", Title: "Fix CI"}
+ const want = "Fix CI (agent Worker Seven) stopped and is waiting on you:\n\nMerge now?\n\nYour answer is sent into that session word for word."
+ if got := nudgeAskQuestion(c, "Merge now?", false); got != want {
+ t.Fatalf("ask text:\n%q\nwant:\n%q", got, want)
+ }
+}
+
+// TestAWaitingSessionAndItsPushNameTheExactTmuxTarget: the queue's waiting
+// session item and its notification body.
+func TestAWaitingSessionAndItsPushNameTheExactTmuxTarget(t *testing.T) {
+ s := whereServer(t)
+ for _, c := range []struct {
+ session, window, source, body string
+ }{
+ {"scratch3", "@37", "mango:1 · tmux scratch3:5 @ ws", "Waiting: mango:1 · tmux scratch3:5 @ ws"},
+ {"build", "@4", "tmux build:7 @ ws", "Waiting: tmux build:7 @ ws"},
+ } {
+ it := whereSessionItem(t, s, c.session, c.window)
+ if it.Source != c.source {
+ t.Errorf("%s: source %q, want %q", c.window, it.Source, c.source)
+ }
+ if got := attentionPushBody(it); got != c.body {
+ t.Errorf("%s: push body %q, want %q", c.window, got, c.body)
+ }
+ }
+}
+
+// whereAsks are a mirrored session's asks: one whose Claude session runs in
+// the labelled window, one in the unlabelled window, one no window runs, and
+// an agent's.
+func whereAsks() []attention.Ask {
+ now := time.Now()
+ return []attention.Ask{
+ {ID: 91, Owner: 1, Asker: attention.Asker{MirrorHost: "ws", MirrorSession: whereLabelledSID, Label: "mango:1 @ ws"}, Question: "Which branch?", CreatedAt: now},
+ {ID: 92, Owner: 1, Asker: attention.Asker{MirrorHost: "ws", MirrorSession: whereUnlabelledSID, Label: "build:7 @ ws"}, Question: "Which branch?", CreatedAt: now},
+ {ID: 93, Owner: 1, Asker: attention.Asker{MirrorHost: "ws", MirrorSession: whereGoneSID, Label: "pear:2 @ ws"}, Question: "Which branch?", CreatedAt: now},
+ {ID: 94, Owner: 1, Asker: attention.Asker{AgentID: 7, Label: "Worker Seven"}, Question: "Which branch?", CreatedAt: now},
+ }
+}
+
+// whereAskSources are whereAsks' sources, by id. 93's window is gone: its
+// stored name, and that no window shows it — never a fabricated target.
+var whereAskSources = map[int64]string{
+ 91: "mango:1 · tmux scratch3:5 @ ws",
+ 92: "tmux build:7 @ ws",
+ 93: "pear:2 @ ws (not shown in any mirrored window now)",
+ 94: "Worker Seven",
+}
+
+// TestAMirroredSessionsAskNamesTheWindowRunningItNow: on the queue (and so its
+// push title), an ask by a mirrored session names the window running its
+// Claude session now; one no window runs says so.
+func TestAMirroredSessionsAskNamesTheWindowRunningItNow(t *testing.T) {
+ s := whereServer(t, whereAsks()...)
+ items, err := s.attentionItems(s.attentionPrincipal(context.Background(), 1))
+ if err != nil {
+ t.Fatal(err)
+ }
+ seen := 0
+ for _, it := range items {
+ if it.Kind != attentionKindAsk {
+ continue
+ }
+ seen++
+ if want := whereAskSources[it.AskID]; it.Source != want {
+ t.Errorf("ask %d: source %q, want %q", it.AskID, it.Source, want)
+ }
+ if want := whereAskSources[it.AskID] + " is asking"; attentionPushTitle(it) != want {
+ t.Errorf("ask %d: push title %q, want %q", it.AskID, attentionPushTitle(it), want)
+ }
+ }
+ if seen != len(whereAskSources) {
+ t.Fatalf("%d asks on the queue, want %d", seen, len(whereAskSources))
+ }
+}
+
+var askPageSource = regexp.MustCompile(`data-ask-source>([^<]*) asks`)
+
+// TestTheAskPageHeaderNamesTheExactTmuxTarget: " asks" on an ask's
+// own page.
+func TestTheAskPageHeaderNamesTheExactTmuxTarget(t *testing.T) {
+ asks := whereAsks()
+ s := whereServer(t, asks...)
+ for _, a := range asks {
+ rec := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", askPagePath(a.ID), nil).WithContext(s.attentionPrincipal(context.Background(), 1))
+ s.renderAskPage(rec, r, a)
+ m := askPageSource.FindStringSubmatch(rec.Body.String())
+ if m == nil {
+ t.Fatalf("ask %d: no header on the page:\n%s", a.ID, rec.Body.String())
+ }
+ if got, want := html.UnescapeString(m[1]), whereAskSources[a.ID]; got != want {
+ t.Errorf("ask %d: header %q asks, want %q asks", a.ID, got, want)
+ }
+ }
+}
+
+// TestASessionPagesAsksBlockNamesTheExactTmuxTarget: the asks block on the
+// labelled window's own page.
+func TestASessionPagesAsksBlockNamesTheExactTmuxTarget(t *testing.T) {
+ s := whereServer(t, whereAsks()...)
+ items := s.askerAsks(s.attentionPrincipal(context.Background(), 1), askerRef{mirror: ui.MirrorRef{Host: "ws", Session: "scratch3", Window: "@37"}})
+ if len(items) != 1 || items[0].AskID != 91 {
+ t.Fatalf("block items %+v, want ask 91 alone", items)
+ }
+ if got, want := items[0].Source, whereAskSources[91]; got != want {
+ t.Fatalf("source %q, want %q", got, want)
+ }
+}
+
+// TestAFiledAsksNotificationNamesTheExactTmuxTarget: the notification sent when
+// a mirrored session's ask is filed (askFiled) — the path that builds its item
+// with no request of its own — names the window running it now. Its stored
+// name is from before the window was renumbered (main:9): the push says main:2.
+func TestAFiledAsksNotificationNamesTheExactTmuxTarget(t *testing.T) {
+ store, pusher := &memPush{}, &rawPusher{}
+ claim(t, store, 1, ep("where-filed"))
+ s := pushTargetServer(t, store, pusher)
+ s.askFiled(attention.Ask{ID: 95, Owner: 1, Asker: attention.Asker{MirrorHost: "ws", MirrorSession: ptOtherSID, Label: "main:9 @ ws"}, Question: "Which branch?"})
+ s.WaitForPushes()
+ var b push.Payload
+ pusher.one(t, ep("where-filed"), &b)
+ if want := "tmux main:2 @ ws is asking"; b.Title != want {
+ t.Fatalf("title %q, want %q", b.Title, want)
+ }
+}
diff --git a/internal/api/cards_attention.go b/internal/api/cards_attention.go
index 18ba7ed5..383f5d39 100644
--- a/internal/api/cards_attention.go
+++ b/internal/api/cards_attention.go
@@ -105,17 +105,27 @@ func (s *Server) cardAttention(ctx context.Context) cardAttentionIndex {
// claudeSessionWindow is the address of host's window running Claude session
// sid, in its active pane or a split (the MCP door's rule, mirrorSessionOf).
func claudeSessionWindow(hosts []mirror.HostView, host, sid string) (ui.MirrorRef, bool) {
+ e, ok := claudeSessionEntry(hosts, host, sid)
+ if !ok {
+ return ui.MirrorRef{}, false
+ }
+ return ui.MirrorRef{Host: e.Host, Session: e.Session, Window: e.Window}, true
+}
+
+// claudeSessionEntry is claudeSessionWindow's window with what names it
+// (Index, Label: mirror.Where), by the same rule.
+func claudeSessionEntry(hosts []mirror.HostView, host, sid string) (mirror.SessionView, bool) {
for _, h := range hosts {
if h.Host != host {
continue
}
for _, w := range h.Windows {
if (w.Claude && w.ClaudeSession == sid) || slices.Contains(w.OtherClaude, sid) {
- return ui.MirrorRef{Host: h.Host, Session: w.Session, Window: w.ID}, true
+ return mirror.SessionView{Host: h.Host, Session: w.Session, Window: w.ID, Index: w.Index, Label: w.Label}, true
}
}
}
- return ui.MirrorRef{}, false
+ return mirror.SessionView{}, false
}
// withCardAttention sets every entry's count (in place).
diff --git a/internal/api/chief_nudge.go b/internal/api/chief_nudge.go
index 0ad51fbb..181275d0 100644
--- a/internal/api/chief_nudge.go
+++ b/internal/api/chief_nudge.go
@@ -341,7 +341,7 @@ func (s *Server) stallInputs(octx context.Context, inFlight map[string]bool) []n
seen[w.ClaudeSession] = true
st := s.ext.Mirror.AgentState(octx, h.Host, w.Session, w.ID)
c := chiefnudge.Session{Kind: chiefnudge.KindMirror, Key: w.ClaudeSession, Host: h.Host,
- TmuxSession: w.Session, Window: w.ID, Label: w.Label, Status: st.Status,
+ TmuxSession: w.Session, Window: w.ID, Index: w.Index, Label: w.Label, Status: st.Status,
Shell: !h.Work[w.ID].IsZero(),
TurnError: st.TurnError != nil || st.HostLimit != nil || st.GuardHeld != "",
OpenAsk: asked["mirror:"+w.ClaudeSession] || inFlight[chiefnudge.KindMirror+":"+w.ClaudeSession]}
@@ -1212,11 +1212,7 @@ func nudgeAskQuestion(c chiefnudge.Session, q string, generic bool) string {
where = "A session"
}
if c.Kind == chiefnudge.KindMirror {
- label := c.Label
- if label == "" {
- label = c.TmuxSession
- }
- where += " (" + label + " @ " + c.Host + ")"
+ where += " (" + mirror.Where(c.Host, c.TmuxSession, c.Window, c.Index, c.Label) + ")"
} else {
where += " (agent " + c.Label + ")"
}
diff --git a/internal/api/chief_nudge_pg_test.go b/internal/api/chief_nudge_pg_test.go
index 32eb0a30..92bf5c02 100644
--- a/internal/api/chief_nudge_pg_test.go
+++ b/internal/api/chief_nudge_pg_test.go
@@ -632,7 +632,7 @@ func TestChiefNudgeLiveAskIsAnsweredAndRelayedVerbatim(t *testing.T) {
}
a, err := f.attn.Get(asUser(authz.OperatorUserID), d.AskID)
if err != nil || !a.Asker.System || a.Asker.Label != chiefNudgeAskLabel ||
- !strings.Contains(a.Question, "Merge into main or into release?") || !strings.Contains(a.Question, "(Gold:1 @ ws)") ||
+ !strings.Contains(a.Question, "Merge into main or into release?") || !strings.Contains(a.Question, "(Gold:1 · tmux main:1 @ ws)") ||
strings.Join(a.Options, "|") != "main|release" {
t.Fatalf("the ask %+v %v", a, err)
}
diff --git a/internal/api/unrouted_push_test.go b/internal/api/unrouted_push_test.go
index 586a7cee..6c0c719d 100644
--- a/internal/api/unrouted_push_test.go
+++ b/internal/api/unrouted_push_test.go
@@ -20,7 +20,7 @@ func TestTheUnroutedAskPushIsCompact(t *testing.T) {
q := unroutedQuestionText("Example inbox", m)
a := attention.Ask{ID: 9, Owner: 1, Asker: attention.Asker{System: true, Label: attention.SystemAskerLabel}, Question: q,
Options: []string{unroutedNoAction}}
- it := askItem(a)
+ it := askItem(a, nil)
if it.Text != q {
t.Fatal("the page's question changed")
}
@@ -31,12 +31,12 @@ func TestTheUnroutedAskPushIsCompact(t *testing.T) {
// A verified sender carries no note; a question that is not muster's
// format (an agent's ask that happens to say "From:") keeps its own text.
m.SenderVerified = true
- if got := attentionPushBody(askItem(attention.Ask{ID: 9, Asker: attention.Asker{System: true}, Question: unroutedQuestionText("Example inbox", m)})); got !=
+ if got := attentionPushBody(askItem(attention.Ask{ID: 9, Asker: attention.Asker{System: true}, Question: unroutedQuestionText("Example inbox", m)}, nil)); got !=
"From: Ada Lovelace · Subject: Invoice 1043 is overdue" {
t.Fatalf("verified: %q", got)
}
agent := attention.Ask{ID: 10, Asker: attention.Asker{AgentID: 3, Label: "w"}, Question: "From: x\nSubject: y — ok?"}
- if got := attentionPushBody(askItem(agent)); got != "From: x Subject: y — ok?" {
+ if got := attentionPushBody(askItem(agent, nil)); got != "From: x Subject: y — ok?" {
t.Fatalf("CONTROL: an agent's question was rewritten: %q", got)
}
}
diff --git a/internal/chiefnudge/chiefnudge.go b/internal/chiefnudge/chiefnudge.go
index f013b120..36685456 100644
--- a/internal/chiefnudge/chiefnudge.go
+++ b/internal/chiefnudge/chiefnudge.go
@@ -148,6 +148,10 @@ type Session struct {
Key string // Claude session id (mirror) or agent id (agent)
// Mirror address (KindMirror).
Host, TmuxSession, Window, Label string
+ // Index is the window's tmux index when the sweep saw it (mirror
+ // Window.Index): display only, for the ask's text (mirror.Where) — tmux
+ // renumbers windows, so nothing is addressed by it.
+ Index int
// AgentID and ChatSessionID (KindAgent): the agent and the ONE conversation
// of it that stalled — an agent can hold several.
AgentID int64
diff --git a/internal/mirror/order.go b/internal/mirror/order.go
index db9f41ee..cad7f20c 100644
--- a/internal/mirror/order.go
+++ b/internal/mirror/order.go
@@ -94,10 +94,51 @@ func (s SessionView) DisplayTitle() string {
// session per name and one window per index in a session — which is what
// makes it the tie-break for two entries of one host that share a Title.
func (s SessionView) Address() string {
- if s.Window != "" {
- return s.Session + ":" + strconv.Itoa(s.Index)
+ return tmuxAddress(s.Session, s.Window, s.Index)
+}
+
+// tmuxAddress is Address's rule, for a caller that holds the window's fields
+// rather than a SessionView (Where).
+func tmuxAddress(session, window string, index int) string {
+ if window != "" {
+ return session + ":" + strconv.Itoa(index)
+ }
+ return session
+}
+
+// Where is the entry's exact place, for every text that sends the operator to
+// it (an ask, a waiting session, a notification): see the function Where.
+func (s SessionView) Where() string {
+ return Where(s.Host, s.Session, s.Window, s.Index, s.Label)
+}
+
+// Where names a mirrored window so the operator can go to it in a terminal:
+// "tmux : @ " — the tmux session name and window
+// index, exactly as tmux's own target syntax takes them (Address) — led by the
+// host's label for it when it has one that is not that same address:
+//
+// tmux build:4 @ laptop
+// mango:1 · tmux scratch3:5 @ laptop
+//
+// A per-session host's entry (window "") is "tmux @ ".
+//
+// 🔴 ONE RULE, ONE PLACE. Every attention surface that names a mirrored window
+// (a chief nudge's ask, a waiting session or held question, the session asker
+// of an ask, their notifications and the ask page's header) takes its words
+// from here. The label is a host's codename and is NOT a tmux name — on its
+// own it named a window nobody could find — so it is never the only name, and
+// the session name alone (no index) is not a target either.
+//
+// The index is a SNAPSHOT: tmux renumbers windows, so text built from it says
+// where the window was when the text was made. Links resolve the window by its
+// Claude session id when followed and stay the source of truth.
+func Where(host, session, window string, index int, label string) string {
+ addr := tmuxAddress(session, window, index)
+ out := "tmux " + addr + " @ " + host
+ if label != "" && label != addr {
+ out = label + QualifierSep + out
}
- return s.Session
+ return out
}
// qualify sets every entry's Qualifier over list — the WHOLE list, across
@@ -170,10 +211,8 @@ func (s SessionView) Title() string {
switch {
case s.Label != "":
return s.Label
- case s.Window != "":
- return s.Session + ":" + strconv.Itoa(s.Index)
}
- return s.Session
+ return s.Address()
}
// HasRenderedView reports whether the entry has a rendered view: a connected
diff --git a/internal/mirror/where_test.go b/internal/mirror/where_test.go
new file mode 100644
index 00000000..2633b00d
--- /dev/null
+++ b/internal/mirror/where_test.go
@@ -0,0 +1,31 @@
+package mirror_test
+
+import (
+ "testing"
+
+ "github.com/ZacxDev/muster/internal/mirror"
+)
+
+// TestWhereIsTheExactTmuxTarget pins mirror.Where whole, for each shape: a
+// window with a codename (its codename's index is NOT its tmux index, so a
+// target built from the label cannot pass), one without, one whose label is
+// its own address, and a per-session host's entry (no window).
+func TestWhereIsTheExactTmuxTarget(t *testing.T) {
+ for _, c := range []struct {
+ name string
+ e mirror.SessionView
+ want string
+ }{
+ {"codename", mirror.SessionView{Host: "laptop", Session: "scratch3", Window: "@37", Index: 5, Label: "mango:1"}, "mango:1 · tmux scratch3:5 @ laptop"},
+ {"no codename", mirror.SessionView{Host: "laptop", Session: "build", Window: "@4", Index: 7}, "tmux build:7 @ laptop"},
+ {"label is the address", mirror.SessionView{Host: "laptop", Session: "build", Window: "@4", Index: 7, Label: "build:7"}, "tmux build:7 @ laptop"},
+ {"per-session host", mirror.SessionView{Host: "laptop", Session: "build"}, "tmux build @ laptop"},
+ } {
+ if got := c.e.Where(); got != c.want {
+ t.Errorf("%s: Where() = %q, want %q", c.name, got, c.want)
+ }
+ if got := mirror.Where(c.e.Host, c.e.Session, c.e.Window, c.e.Index, c.e.Label); got != c.want {
+ t.Errorf("%s: Where(...) = %q, want %q", c.name, got, c.want)
+ }
+ }
+}
From 795d7c910731ecd851de7fa5d8b01776b28f7116 Mon Sep 17 00:00:00 2001
From: Zachary Lowden
Date: Sat, 31 Oct 2026 23:40:29 -0500
Subject: [PATCH 2/7] refactor(attention): keep askItem's and
nudgeAskQuestion's signatures for the in-flight PRs
Test-merged with the two open PRs that touch these functions: one changes
nudgeAskQuestion's parameters, the other's test calls askItem(a). So:
- askItem takes its hosts variadically: askItem(a) still compiles (a mirrored
session's ask is then named as no window showing it); every production
caller passes the owner's windows. The test call sites go back to main's.
- The nudge ask's "where" is its own function, nudgeAskWhere, placed away
from nudgeAskQuestion; nudgeAskQuestion's only change is calling it. The
unit tests pin nudgeAskWhere whole; the live-ask PG test now pins the whole
question built through the real path.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
internal/api/attention.go | 9 ++++----
internal/api/attention_ask_boxes_test.go | 6 +++---
internal/api/attention_asker.go | 2 +-
internal/api/attention_push_target_test.go | 10 ++++-----
internal/api/attention_test.go | 4 ++--
internal/api/attention_where_test.go | 20 +++++++++--------
internal/api/chief_nudge.go | 25 ++++++++++++++--------
internal/api/chief_nudge_pg_test.go | 2 +-
internal/api/unrouted_push_test.go | 6 +++---
9 files changed, 47 insertions(+), 37 deletions(-)
diff --git a/internal/api/attention.go b/internal/api/attention.go
index 54e38d50..f241034a 100644
--- a/internal/api/attention.go
+++ b/internal/api/attention.go
@@ -308,12 +308,13 @@ func (s *Server) askItemNow(ctx context.Context, a attention.Ask) attentionItem
if askNeedsHosts(a) && s.ext.Mirror != nil {
hosts = s.ext.Mirror.Hosts(ctx)
}
- return askItem(a, hosts)
+ return askItem(a, hosts...)
}
// askItem is ask a as an attention item; hosts is its owner's windows, which
-// a mirrored session's ask names its source from (askSource).
-func askItem(a attention.Ask, hosts []mirror.HostView) attentionItem {
+// a mirrored session's ask names its source from (askSource) — none given, it
+// is named as no window showing it.
+func askItem(a attention.Ask, hosts ...mirror.HostView) attentionItem {
it := attentionItem{Key: attentionAskKey(a.ID), Kind: attentionKindAsk, Source: askSource(a, hosts), Text: a.Question,
Options: a.Options, Since: a.CreatedAt, Link: attentionPath + "#" + attentionAskKey(a.ID), AskID: a.ID, suggestions: a.Suggestions}
if a.Asker.System {
@@ -581,7 +582,7 @@ func (s *Server) attentionItems(ctx context.Context) ([]attentionItem, error) {
if askNeedsHosts(a) && !listed && s.ext.Mirror != nil {
hosts, listed = s.ext.Mirror.Hosts(ctx), true
}
- out = append(out, askItem(a, hosts))
+ out = append(out, askItem(a, hosts...))
}
}
out = append(out, s.waitingSessions(ctx)...)
diff --git a/internal/api/attention_ask_boxes_test.go b/internal/api/attention_ask_boxes_test.go
index 0ebf8d3c..b5cae29e 100644
--- a/internal/api/attention_ask_boxes_test.go
+++ b/internal/api/attention_ask_boxes_test.go
@@ -76,7 +76,7 @@ func boxServer(t *testing.T, dictate bool) *Server {
}
func systemAsk(id int64) attentionItem {
- return askItem(attention.Ask{ID: id, Owner: 1, Asker: attention.Asker{System: true, Label: chiefNudgeAskLabel}, Question: "q", CreatedAt: time.Now()}, nil)
+ return askItem(attention.Ask{ID: id, Owner: 1, Asker: attention.Asker{System: true, Label: chiefNudgeAskLabel}, Question: "q", CreatedAt: time.Now()})
}
// TestAChiefNudgesAskLinksTheSessionItIsAbout: a nudge's ask is muster's own
@@ -148,8 +148,8 @@ var dictateTag = regexp.MustCompile(`