Response viewer stays pinned to the launch conversation for a pane driven from tmux
Version 1.24.1 · Install git clone · Mode claude (CLI 2.1.251) · OS Linux (WSL2) · Surface Response Viewer / GET /api/sessions/:id/last-response
What you see
I attach to a pane's tmux session directly (tmux -L codeman attach -t codeman-<id8>) rather
than typing into Codeman's web terminal. After a /clear, the response viewer keeps showing
the pre-/clear transcript, permanently — not one turn behind, but forever.
Why
resolveActiveClaudeSessionIdFromHistory() (src/web/routes/session-routes.ts) is the only
thing that can notice a /clear, and it opens with:
const submitAt = session.lastSubmitAt;
if (!submitAt) return null; // never typed through Codeman — nothing to credit
_lastSubmitAt is bumped only by _trackSubmit(), reachable only from Session.write() and
Session.writeViaMux() — i.e. input that flows through Codeman. A pane driven straight
from tmux never sets it, so on my machine ~/.codeman/state.json carries no lastSubmitAt
for either live Claude pane and 100% of calls return at that line.
The docs already anticipate the correlation being unavailable and choose the safe behaviour
("a viewer one turn behind beats a viewer showing someone else's conversation"), so this is
not a wrong-conversation bug — it is a never-updates bug for a whole class of user.
Why the obvious fix is wrong
Relaxing the guard — dropping it, or synthesising an anchor from PTY idle→busy — recreates
exactly the bug session-routes.ts documents as already made and reverted: keying off
"newest history entry for this cwd" made the eye follow a stranger's conversation. Measured
on one folder here, three live panes share a workingDir and the minimum gap between
adjacent prompts of two different conversations is 2.3 s, well inside
CLAUDE_SUBMIT_MATCH_MS = 10_000.
Proposed fix
Add a UserPromptSubmit hook. Its payload carries session_id — the pane's live
conversation, reported from inside the CLI process — and it reaches Codeman addressed by that
pane's own $CODEMAN_SESSION_ID. That binding is a fact, not a correlation: it never
consults workingDir, so no sibling pane can claim it. A pane holding such an id skips the
correlation entirely, so the number of prompts eligible for cwd-based guessing goes down,
never up. The hook also stamps lastSubmitAt, so it finally means "a prompt was submitted".
One implementation note worth stating up front, because it is easy to get wrong: the hook's
stdout must be discarded with curl's own -o /dev/null. Claude Code injects a
UserPromptSubmit hook's stdout into the model's context ("Exit code 0 - stdout shown to
Claude" in the CLI's own hook reference), and a trailing >/dev/null does not work — the
existing curlCmd ends … 2>/dev/null || true, and in pipeline || true >/dev/null the
shell binds the redirection to true, which never runs on the success path.
Branch is ready with tests and the full gate green — happy to open the PR.
Response viewer stays pinned to the launch conversation for a pane driven from tmux
Version 1.24.1 · Install git clone · Mode claude (CLI 2.1.251) · OS Linux (WSL2) · Surface Response Viewer /
GET /api/sessions/:id/last-responseWhat you see
I attach to a pane's tmux session directly (
tmux -L codeman attach -t codeman-<id8>) ratherthan typing into Codeman's web terminal. After a
/clear, the response viewer keeps showingthe pre-
/cleartranscript, permanently — not one turn behind, but forever.Why
resolveActiveClaudeSessionIdFromHistory()(src/web/routes/session-routes.ts) is the onlything that can notice a
/clear, and it opens with:_lastSubmitAtis bumped only by_trackSubmit(), reachable only fromSession.write()andSession.writeViaMux()— i.e. input that flows through Codeman. A pane driven straightfrom tmux never sets it, so on my machine
~/.codeman/state.jsoncarries nolastSubmitAtfor either live Claude pane and 100% of calls return at that line.
The docs already anticipate the correlation being unavailable and choose the safe behaviour
("a viewer one turn behind beats a viewer showing someone else's conversation"), so this is
not a wrong-conversation bug — it is a never-updates bug for a whole class of user.
Why the obvious fix is wrong
Relaxing the guard — dropping it, or synthesising an anchor from PTY idle→busy — recreates
exactly the bug
session-routes.tsdocuments as already made and reverted: keying off"newest history entry for this cwd" made the eye follow a stranger's conversation. Measured
on one folder here, three live panes share a
workingDirand the minimum gap betweenadjacent prompts of two different conversations is 2.3 s, well inside
CLAUDE_SUBMIT_MATCH_MS = 10_000.Proposed fix
Add a
UserPromptSubmithook. Its payload carriessession_id— the pane's liveconversation, reported from inside the CLI process — and it reaches Codeman addressed by that
pane's own
$CODEMAN_SESSION_ID. That binding is a fact, not a correlation: it neverconsults
workingDir, so no sibling pane can claim it. A pane holding such an id skips thecorrelation entirely, so the number of prompts eligible for cwd-based guessing goes down,
never up. The hook also stamps
lastSubmitAt, so it finally means "a prompt was submitted".One implementation note worth stating up front, because it is easy to get wrong: the hook's
stdout must be discarded with curl's own
-o /dev/null. Claude Code injects aUserPromptSubmithook's stdout into the model's context ("Exit code 0 - stdout shown toClaude" in the CLI's own hook reference), and a trailing
>/dev/nulldoes not work — theexisting
curlCmdends… 2>/dev/null || true, and inpipeline || true >/dev/nulltheshell binds the redirection to
true, which never runs on the success path.Branch is ready with tests and the full gate green — happy to open the PR.