Skip to content

Chat | Allow new conversations while a stream is active - #48

Merged
BrianGenisio merged 2 commits into
mainfrom
fix/new-chat-during-stream
Aug 7, 2026
Merged

Chat | Allow new conversations while a stream is active#48
BrianGenisio merged 2 commits into
mainfrom
fix/new-chat-during-stream

Conversation

@BrianGenisio

@BrianGenisio BrianGenisio commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Starting a second conversation while another reply was still streaming felt blocked: New chat waited on octavus.agentSessions.create, and that call could stall until the open trigger finished. Clicked threads then appeared in a burst when the stream ended.

This restores the multiplex intent: you can open and use another conversation while one is still responding.

Changes

New chat switches the UI immediately with a local pending thread, then finishes Octavus create in the background. Send/upload on that thread wait for the real session id before talking to Octavus.

Session create now goes through a dedicated undici Agent (lib/octavus-create.js) so a long-lived /api/trigger fetch on the default dispatcher cannot starve create. Worth a close look if you know Octavus connection behavior.

chat-sessions.json updates are queued (lib/sessions-file.js) so overlapping create/save/delete/fork writers cannot clobber each other under parallel streams.

Test plan

  • Start a long reply, click New chat before it finishes: a new sidebar thread appears immediately and the empty composer is usable
  • Send on the new thread while the first is still streaming: both can proceed (up to the existing 5-stream cap)
  • Delete a pending New chat thread before create resolves: no orphaned sidebar entry; late create is discarded
  • npm test

New chat was waiting on Octavus session create, which could stall behind an
in-flight trigger. Switch the UI optimistically and create sessions on a
dedicated HTTP pool so parallel conversations stay usable.

Co-authored-by: Cursor <cursoragent@cursor.com>
@BrianGenisio
BrianGenisio marked this pull request as ready for review August 7, 2026 20:53
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a100b3c-f2f8-42d9-bfae-abaeb487c89f

📥 Commits

Reviewing files that changed from the base of the PR and between 7612a67 and 53e96dd.

📒 Files selected for processing (7)
  • lib/octavus-create.js
  • public/app.js
  • server.js
  • tests/dom/harness.js
  • tests/dom/render.test.js
  • tests/octavus-create.test.js
  • tests/server.test.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/server.test.js
  • tests/dom/render.test.js
  • tests/octavus-create.test.js
  • server.js
  • lib/octavus-create.js
  • public/app.js

📝 Walkthrough

Walkthrough

The server now creates Octavus agent sessions through a dedicated Undici helper. Session-file updates execute in sequence across creation, resume, deletion, forking, and saving. The client displays new chats immediately as pending sessions. Sends and uploads wait for session creation before attaching the chat runtime. Pending-session deletion and creation failures clean up local and remote state. Tests cover API requests, serialized writes, server persistence, and delayed UI session creation.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes allowing new conversations while another conversation is streaming, which is the main change.
Description check ✅ Passed The description directly explains optimistic new chats, background session creation, connection handling, queued writes, and testing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
public/app.js (1)

1518-1526: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The composer is cleared before creation is resolved, so a failed create discards the user's message.

Lines 1518-1521 clear promptInput and the attachment list. Line 1524 then awaits ensureActiveReady(). If creation fails, ensureActiveReady() returns null, and line 1525 returns without sending. The typed text and the ready file refs are already gone, and no error is shown to the user.

Before this change, isComposerSendAllowed() required active.chat, so send never started without a runtime. Line 1706 removes that guarantee. Restore the composer content when the send does not start.

🐛 Proposed fix
   promptInput.value = '';
   const filesToSend = readyRefs;
   clearAttachment();
   updateSendBtn();
 
   try {
     const rt = await ensureActiveReady();
-    if (!rt?.chat) return;
+    if (!rt?.chat) {
+      // Creation failed or the thread was abandoned. Give the input back.
+      promptInput.value = text;
+      updateSendBtn();
+      announceChatStatus(t('Could not start the conversation. Try again.'));
+      return;
+    }
     await rt.chat.send(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/app.js` around lines 1518 - 1526, Restore the composer state when
sending cannot start: in the send flow around ensureActiveReady, preserve the
original prompt text and readyRefs before clearing them, then restore the prompt
and attachment references when ensureActiveReady returns no chat or creation
fails. Ensure failed runtime creation does not discard the user’s message or
files, while retaining the existing clear behavior after a send successfully
starts.
🧹 Nitpick comments (5)
tests/server.test.js (1)

228-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the creation-failure path.

The suite covers only the success path. createAgentSession now throws on non-OK responses and on a missing sessionId. The client in public/app.js depends on a non-2xx response to trigger the pending-thread rollback at lines 2040-2058. Assert that POST /api/sessions returns an error status when the helper rejects, and that nothing is written to the sessions file.

🧪 Proposed test
+  it('returns an error status and persists nothing when create fails', async () => {
+    mockSessionsFile({ sessions: [] });
+    createAgentSession.mockRejectedValue(new Error('upstream down'));
+
+    const res = await request(app).post('/api/sessions').send({});
+
+    expect(res.status).toBeGreaterThanOrEqual(500);
+    expect(fs.writeFile).not.toHaveBeenCalled();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server.test.js` around lines 228 - 254, Add a failure-path test in the
POST /api/sessions suite that makes createAgentSession reject, then assert the
endpoint returns a non-2xx error response and fs.write is not called, confirming
no session record is persisted when creation fails.
tests/octavus-create.test.js (1)

48-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the missing sessionId branch.

createAgentSession rejects when the API returns 200 with no string sessionId (lib/octavus-create.js lines 47-50). No test exercises that branch. This is the validation that protects buildSessionRecord in server.js from an undefined session id.

🧪 Proposed test
+  it('throws when the API returns no sessionId', async () => {
+    fetchMock.mockResolvedValue({
+      ok: true,
+      json: async () => ({}),
+    });
+
+    await expect(
+      createAgentSession({ baseUrl: 'https://octavus.example', agentId: 'agent-1' }),
+    ).rejects.toThrow(/sessionId/);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/octavus-create.test.js` around lines 48 - 68, Add a test alongside the
existing createAgentSession rejection cases that mocks a successful 200 API
response without a string sessionId and asserts createAgentSession rejects. Use
the existing fetchMock and request options, and verify the rejection covers the
validation error protecting buildSessionRecord.
server.js (1)

139-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the false sentinel in the updater contract.

updateSessionsFile supports three distinct mutator return shapes: a new object, undefined (write the mutated data), and false (skip the write). No caller in this diff uses false, and the JSDoc does not describe it. A future caller that returns a falsy value by accident will silently skip persistence. Document the contract, or drop the sentinel until a caller needs it.

📝 Proposed documentation
-/** Run a read-modify-write against chat-sessions.json without overlapping writers. */
+/**
+ * Run a read-modify-write against chat-sessions.json without overlapping writers.
+ *
+ * `@param` {(data: object) => object|false|void|Promise<object|false|void>} mutator
+ *   Return the next state to persist, `undefined` to persist the mutated input,
+ *   or `false` to skip the write.
+ * `@returns` {Promise<object>} The persisted (or unchanged) sessions data.
+ */
 function updateSessionsFile(mutator) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 139 - 147, Update the JSDoc for updateSessionsFile to
document the mutator return contract: returning a new object writes it,
returning undefined writes the existing mutated data, and returning false skips
persistence. Keep the current false-sentinel behavior unchanged.
tests/dom/render.test.js (1)

462-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the pending id is replaced by the real session id.

The test confirms the thread count is stable, but it does not confirm the id swap at public/app.js lines 2018-2030. A regression that leaves the pending-* id in allSessionsMeta still passes. That id is the value the delete path at line 1879 uses to decide whether to call the server, so the swap is the important post-condition.

Consider also adding a case for the failure path, where POST /api/sessions returns a non-OK status. That case exercises the rollback at lines 2040-2058.

🧪 Proposed assertion
     // Create finished → runtime wired with a real chat instance.
     expect(fakeChats.length).toBe(chatsBefore + 1);
     expect(qa('.session-item').length).toBe(before + 1);
+    // The optimistic id is replaced by the real one, so delete hits the server.
+    q('.session-item .session-item__delete').click();
+    await settle();
+    expect(requests.some((r) => r.url === '/api/sessions/session-2' && r.method === 'DELETE')).toBe(true);

Destructure requests from bootApp() at line 448 to use this assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/dom/render.test.js` around lines 462 - 468, Extend the test around
releaseNewSession to assert that allSessionsMeta no longer contains the
pending-* id and instead contains the real session id returned by the
session-creation request, using the requests captured from bootApp(). Add a
failure-path case where POST /api/sessions returns a non-OK response and verify
the pending session metadata is rolled back.
lib/octavus-create.js (1)

12-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an explicit timeout to session creation.

Undici 8.10.0 defaults headersTimeout and bodyTimeout to 300,000 ms. Configure a shorter timeout, such as 15,000 ms, on createAgent. Use AbortSignal.timeout(15_000) if the request requires a total deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/octavus-create.js` around lines 12 - 38, Configure createAgent with a
15,000 ms headersTimeout and bodyTimeout for session creation, or apply
AbortSignal.timeout(15_000) to the undiciFetch request if a total request
deadline is required. Keep the existing dispatcher usage in createAgentSession
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@public/app.js`:
- Around line 2040-2060: Update the startNewChat() catch cleanup to return null
instead of rethrowing, so callers receive a resolved failure result after
cleanup. In the active-session fallback, attach handling to the switchSession()
promise to prevent unhandled rejections while preserving the best-effort
behavior; keep the existing cleanup and rendering paths unchanged.

---

Outside diff comments:
In `@public/app.js`:
- Around line 1518-1526: Restore the composer state when sending cannot start:
in the send flow around ensureActiveReady, preserve the original prompt text and
readyRefs before clearing them, then restore the prompt and attachment
references when ensureActiveReady returns no chat or creation fails. Ensure
failed runtime creation does not discard the user’s message or files, while
retaining the existing clear behavior after a send successfully starts.

---

Nitpick comments:
In `@lib/octavus-create.js`:
- Around line 12-38: Configure createAgent with a 15,000 ms headersTimeout and
bodyTimeout for session creation, or apply AbortSignal.timeout(15_000) to the
undiciFetch request if a total request deadline is required. Keep the existing
dispatcher usage in createAgentSession unchanged.

In `@server.js`:
- Around line 139-147: Update the JSDoc for updateSessionsFile to document the
mutator return contract: returning a new object writes it, returning undefined
writes the existing mutated data, and returning false skips persistence. Keep
the current false-sentinel behavior unchanged.

In `@tests/dom/render.test.js`:
- Around line 462-468: Extend the test around releaseNewSession to assert that
allSessionsMeta no longer contains the pending-* id and instead contains the
real session id returned by the session-creation request, using the requests
captured from bootApp(). Add a failure-path case where POST /api/sessions
returns a non-OK response and verify the pending session metadata is rolled
back.

In `@tests/octavus-create.test.js`:
- Around line 48-68: Add a test alongside the existing createAgentSession
rejection cases that mocks a successful 200 API response without a string
sessionId and asserts createAgentSession rejects. Use the existing fetchMock and
request options, and verify the rejection covers the validation error protecting
buildSessionRecord.

In `@tests/server.test.js`:
- Around line 228-254: Add a failure-path test in the POST /api/sessions suite
that makes createAgentSession reject, then assert the endpoint returns a non-2xx
error response and fs.write is not called, confirming no session record is
persisted when creation fails.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb09b582-9312-4a75-bc15-0654b914c5ad

📥 Commits

Reviewing files that changed from the base of the PR and between dac792f and 7612a67.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • lib/octavus-create.js
  • lib/sessions-file.js
  • package.json
  • public/app.js
  • server.js
  • tests/dom/harness.js
  • tests/dom/render.test.js
  • tests/octavus-create.test.js
  • tests/server.test.js
  • tests/sessions-file.test.js

Comment thread public/app.js
Restore composer content when session create fails before send, settle
createPromise to null instead of rejecting, and cover the review nits with
timeouts plus create-failure tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@BrianGenisio
BrianGenisio merged commit 2085792 into main Aug 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant