let every client watching a chat receive it, not the newest one
Reported from two devices at once: typing on the iPad, reading the reply on the Mac. Sending from the Mac produced nothing there. Both halves are one field. A session held `ws`, a single socket, and `attachWs` assigned it. So the newest attach silently took the turn away from whoever was already watching — and with a tab now holding up to three panes, plus a phone and a laptop on the same conversation, several sockets per session stopped being exotic and became the ordinary case. Now a Set, and every message goes to all of them. `detachWs(sessionId)` was worse, because it named no socket: it nulled the field on ANY close. A stale client going away therefore killed delivery for the client that had attached after it, which is the "nothing happens on the Mac" half. It takes the socket now and removes only that one, and the idle GC is armed only once nothing is left watching — otherwise a close would collect a session another pane is still reading. endTurnIfAgentIsGone takes the whole set for the same reason: a cut-off notice explains a spinner that will otherwise never stop, and telling one of three clients leaves two spinning. Typecheck clean. 600 pass, 2 fail — cliamp path-escape and the pty transport test, both failing identically on master before this change. Nobody has clicked it; the two devices that reported it are the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ class SessionManager {
|
||||
cwd,
|
||||
model,
|
||||
piProcess: null,
|
||||
ws: null,
|
||||
sockets: new Set(),
|
||||
lastActivity: Date.now(),
|
||||
idleTimer: null,
|
||||
streamBuffer: '',
|
||||
@@ -145,7 +145,7 @@ class SessionManager {
|
||||
attachWs(sessionId: string, ws: any): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.ws = ws;
|
||||
session.sockets.add(ws);
|
||||
session.lastActivity = Date.now();
|
||||
|
||||
if (session.idleTimer) {
|
||||
@@ -155,14 +155,24 @@ class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
detachWs(sessionId: string): void {
|
||||
/**
|
||||
* Removes one socket. The caller must say WHICH — a bare `detachWs(sessionId)` used to null the
|
||||
* session's only socket field, so a stale client's close event silenced whichever client had attached
|
||||
* after it. A close is only the end of the conversation when nothing else is still watching.
|
||||
*/
|
||||
detachWs(sessionId: string, ws: any): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.ws = null;
|
||||
session.sockets.delete(ws);
|
||||
session.lastActivity = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether anything is still watching — the idle GC must not start while another client is attached. */
|
||||
hasSockets(sessionId: string): boolean {
|
||||
return (this.sessions.get(sessionId)?.sockets.size ?? 0) > 0;
|
||||
}
|
||||
|
||||
setIdleTimeout(sessionId: string, timeoutMs: number): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
Reference in New Issue
Block a user