Skip to content

fix(desktop): bind a tab's slash command to its own session - #71805

Merged
OutThisLife merged 2 commits into
mainfrom
bb/slash-busy-queue
Jul 26, 2026
Merged

fix(desktop): bind a tab's slash command to its own session#71805
OutThisLife merged 2 commits into
mainfrom
bb/slash-busy-queue

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

A slash command typed in a ⌘T tab or a split pane runs through the primary chat's dispatcher, which read the foreground view's identity for two decisions it had no business asking the foreground about. Both are fixed here by reading the target session's own published state.

Busy

The gate at slash.ts read busyRef — a mirror of $busy, which tracks whatever chat is on screen. A brand-new tab (openNewSessionTile('center', { listed: false }): fresh backend session, zero turns) reported

/work · ⚡ loading skill: work
/work · session busy — message queued to send when the current turn finishes

because an unrelated chat happened to be mid-stream. The converse leaked too: a background or tile send could fire into a live turn whenever the foreground was idle.

submit.ts had the same guard against the same flag.

Identity

withSlashOutput bound its output writer to selectedStoredSessionIdRef — again the foreground. Passing that stored id into updateSessionState makes ensureSessionState treat it as a stored-id rotation, so a tab's transcript writes re-key its cache entry onto the primary's stored session and remap runtimeIdByStoredSessionId with it. The queue key derived from the same value, so a queued kickoff parked on the primary's queue and useBackgroundQueueDrain would later fire it into the wrong conversation.

submitText also dropped options.sessionId when it routed to a slash command, so a background queue drain's explicit target was silently swapped for the foreground session.

Shape

isTargetSessionBusy in utils.ts reads the target's published $sessionStates entry and falls back to the foreground flag only when the target has no state yet — a just-minted session whose first publish hasn't landed. Submit and slash both call it, so they can't drift apart again.

Verification

apps/desktop: 132 tests in use-prompt-actions pass, 567 across src/app/session/hooks, src/app/chat, and the composer queue. tsc --noEmit clean. Three new tests cover the idle-target, busy-target, and tile-binding cases; the tile-binding one fails on the parent commit and passes here.

Prior art

Distinct from #44978 / #45030, which are about a genuinely busy session having no escape hatch — this is a session that isn't busy at all being told it is. No open PR touches these files.

A slash command runs against the session `resolveTargetSessionId` picks,
which is routinely not the session on screen — a tile, a route rebind, or
a session created by the call itself. Both prompt pipelines gated on
`busyRef`, the FOREGROUND view's busy flag, so one session's send was
gated on another session's turn: a stale foreground `true` (a warm resume
of a still-running chat leaves one behind) parked an idle session's
command on the composer queue and reported "session busy" about a session
doing nothing. The converse also leaked — a background send could fire
mid-turn while the foreground happened to be idle.

Read the published per-session state instead, falling back to the
foreground flag only when the target has no state yet (a just-minted
session whose first publish hasn't landed). One shared resolver so submit
and slash cannot drift apart again.
A slash command in a ⌘T tab or split pane routes through the primary
chat's dispatcher, which read the FOREGROUND view's identity for two
decisions it had no business asking the foreground about.

Busy: the gate read `busyRef`, a mirror of whatever chat is on screen.
A brand-new tab with zero turns was told "session busy — message queued"
because an unrelated chat was mid-stream, and the converse let a
background send fire into a live turn.

Identity: the output writer bound to the foreground's stored session, so
a tab's transcript writes re-keyed its cache entry onto the primary's
stored id and its queued payload landed on the primary's queue — the
kickoff would then drain into the wrong conversation. `submitText` also
dropped an explicit target when it routed to a slash command, running a
queue drain's command against whatever was in front.

Read the target session's own published state for both. One shared
resolver so submit and slash cannot drift apart again.
@OutThisLife
OutThisLife enabled auto-merge July 26, 2026 06:45
@OutThisLife
OutThisLife merged commit 67380ba into main Jul 26, 2026
30 checks passed
@OutThisLife
OutThisLife deleted the bb/slash-busy-queue branch July 26, 2026 06:50
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 428c909

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence is publishing...

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #71605. #71605 fixes durable-session target resolution; this patch separately fixes foreground-vs-target busy, output, queue, and submit-state binding for tabs and split panes.

OutThisLife added a commit that referenced this pull request Jul 26, 2026
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and
printed "⚡ loading skill: work" there, then fired the skill's kickoff
prompt as a user message into whatever conversation was on screen.

The dispatcher resolves its target once, through resolveTargetSessionId,
and every other consumer of that answer already honors it: the output
writer binds to the target's stored id, and the busy gate reads the
target's own state. The send did not — `submitPromptText(message)` passed
no target at all, so submit fell back to `activeSessionIdRef`, which
names the foreground chat. #71805 fixed the two sibling leaks in this
same function; this is the third and the one that actually moved the
user's prompt.

Forward the resolved pair instead. Every target the dispatcher serves —
a tile, a background queue drain, a session this very call created —
was hitting the same fallback, so the fix covers the class rather than
the tab case that surfaced it.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…-queue

fix(desktop): bind a tab's slash command to its own session
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and
printed "⚡ loading skill: work" there, then fired the skill's kickoff
prompt as a user message into whatever conversation was on screen.

The dispatcher resolves its target once, through resolveTargetSessionId,
and every other consumer of that answer already honors it: the output
writer binds to the target's stored id, and the busy gate reads the
target's own state. The send did not — `submitPromptText(message)` passed
no target at all, so submit fell back to `activeSessionIdRef`, which
names the foreground chat. NousResearch#71805 fixed the two sibling leaks in this
same function; this is the third and the one that actually moved the
user's prompt.

Forward the resolved pair instead. Every target the dispatcher serves —
a tile, a background queue drain, a session this very call created —
was hitting the same fallback, so the fix covers the class rather than
the tab case that surfaced it.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and
printed "⚡ loading skill: work" there, then fired the skill's kickoff
prompt as a user message into whatever conversation was on screen.

The dispatcher resolves its target once, through resolveTargetSessionId,
and every other consumer of that answer already honors it: the output
writer binds to the target's stored id, and the busy gate reads the
target's own state. The send did not — `submitPromptText(message)` passed
no target at all, so submit fell back to `activeSessionIdRef`, which
names the foreground chat. NousResearch#71805 fixed the two sibling leaks in this
same function; this is the third and the one that actually moved the
user's prompt.

Forward the resolved pair instead. Every target the dispatcher serves —
a tile, a background queue drain, a session this very call created —
was hitting the same fallback, so the fix covers the class rather than
the tab case that surfaced it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants