fix(tui): stop a stream outlived by /new from leaking into the next conversation
package / wheel (pull_request) Waiting to run

_finish_stream appended the completed reply to self.history - whatever
list that name currently pointed at - not to the conversation the
stream was actually answering. /new reassigns self.history to a fresh
list; a stream still running when that happens finished by silently
appending the old conversation's trailing reply onto the new one, which
then rides along in that new conversation's next /chat/stream history
payload. The conversation_id was already captured by closure for this
exact reason (see the tool-denial path); self.history needed the same
treatment.
This commit is contained in:
Jon Wingender
2026-08-26 14:01:19 -05:00
23 changed files with 1803 additions and 125 deletions
+10 -5
View File
@@ -359,14 +359,19 @@ class NexusTUI:
if not self.conversation_id:
self.conversation_id = str(uuid.uuid4())
conversation_id = self.conversation_id
# The list object itself, not self.history - /new reassigns
# self.history to a fresh list, and a stream that outlives that
# must keep appending its reply to the conversation it actually
# belongs to, not whatever self.history now points at.
history_ref = self.history
body: dict[str, Any] = {
"message": message,
"conversation_id": conversation_id,
"history": list(self.history),
"history": list(history_ref),
}
if self._model:
body["model"] = self._model
self.history.append({"role": "user", "content": message})
history_ref.append({"role": "user", "content": message})
async def stream_worker():
reply_parts: list[str] = []
@@ -474,19 +479,19 @@ class NexusTUI:
if self._stream_cancel == (loop, task):
self._stream_cancel = None
text = "".join(reply_parts).strip()
self._call_ui(self._finish_stream, text)
self._call_ui(self._finish_stream, text, history_ref)
threading.Thread(
target=lambda: asyncio.run(stream_worker()), daemon=True
).start()
def _finish_stream(self, text: str) -> None:
def _finish_stream(self, text: str, history_ref: list) -> None:
log = self.query_one("#log", RichLog)
live = self.query_one("#live", Static)
try:
if text:
log.write(format_assistant_line(text))
self.history.append(
history_ref.append(
{"role": "assistant", "content": text}
)
finally: