fix(tui): stop a stream outlived by /new from leaking into the next conversation #13

Merged
enderofwings merged 6 commits from fix/tui-history-leak into main 2026-08-26 19:04:59 +00:00
Owner

Carries #8 (git.enderofwings.com/enderofwings/NexusOS/pulls/8, Athena) forward with one fix found in review: _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. conversation_id was already captured by closure for exactly this reason (the tool-denial path); self.history needed the same treatment. Covered by a new regression test.

Everything else in this PR held up under review: the tool-approval deny handshake genuinely blocks server-side (no auto-approve-on-timeout), the capability token is never logged/persisted, and cancellation doesn't leak connections. Two low-severity items noted but not blocking: the deny POST uses a synchronous client inside the async stream worker (delays - never skips - an interrupt during an in-flight deny by up to a few seconds), and RichLog/history have no cap over a very long session.

Built on 8/head (9ed2908) plus this one commit, merged forward onto current main. Merge this instead of #8.

Carries #8 (git.enderofwings.com/enderofwings/NexusOS/pulls/8, Athena) forward with one fix found in review: `_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. `conversation_id` was already captured by closure for exactly this reason (the tool-denial path); `self.history` needed the same treatment. Covered by a new regression test. Everything else in this PR held up under review: the tool-approval deny handshake genuinely blocks server-side (no auto-approve-on-timeout), the capability token is never logged/persisted, and cancellation doesn't leak connections. Two low-severity items noted but not blocking: the deny POST uses a synchronous client inside the async stream worker (delays - never skips - an interrupt during an in-flight deny by up to a few seconds), and RichLog/history have no cap over a very long session. Built on 8/head (9ed2908) plus this one commit, merged forward onto current main. Merge this instead of #8.
enderofwings added 6 commits 2026-08-26 19:02:29 +00:00
Add a Textual chat interface with threaded SSE streaming, slash commands, interrupt handling, and bare nexus dispatch. Package it behind the tui extra, document usage, and cover command routing, dependencies, and headless interaction with tests.
Keep stream failures in the persistent transcript instead of clearing them with the live preview. Use each tool request's capability token to deny actions immediately until the TUI has an interactive approval flow, and cover both behaviors with focused regressions.
Capture each stream's conversation ID before starting its worker so /new cannot redirect a later action denial. Add a headless regression that mutates the active conversation while a tool request is in flight.
Move chat streaming onto a cancellable async task so Ctrl+C interrupts a pending socket read on macOS instead of waiting for the 120-second read timeout. Add a headless silent-stream regression that verifies prompt recovery and a successful next message.
fix(tui): prioritize interrupt and quit keys
package / wheel (pull_request) Waiting to run
9ed2908170
Declare Ctrl+C and Ctrl+D as priority Textual bindings so the focused prompt cannot consume them. Drive exit and silent-stream cancellation regressions through Pilot key events instead of calling action handlers directly.
_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.
enderofwings merged commit e60ed13361 into main 2026-08-26 19:04:59 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: enderofwings/NexusOS#13