Skip to content

feat: stream interrupt, session recovery, and resume (Phase 03)#15

Merged
moyunzero merged 3 commits into
masterfrom
gsd/phase-03-stream-reliability
Jun 28, 2026
Merged

feat: stream interrupt, session recovery, and resume (Phase 03)#15
moyunzero merged 3 commits into
masterfrom
gsd/phase-03-stream-reliability

Conversation

@moyunzero

@moyunzero moyunzero commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Esc interrupt: Partial assistant text is preserved in the transcript; composer restores before first token; running bash/MCP tools show Interrupted by user and subprocesses are killed immediately.
  • Session recovery: Auto-resume when reopening a session whose last message is user-only; partial assistant tails require manual /resume (regenerate from last user turn).
  • Server stream reliability: Active stream registry, abort endpoint, and stream buffer for SaaS mode reconnect/resume.
  • Docs & hygiene: Session recovery documented in README (EN/ZH); .gitignore expanded for local-only paths (.mocode/, .agents/, tmp/, etc.); Karpathy behavioral guidelines added to AGENTS.md.

Test plan

  • bun test packages/cli packages/server — 193 tests pass
  • Esc during streaming text — partial text kept, dim footer (no INTERRUPTED banner)
  • Esc before first token — composer text restored
  • Esc during bash/MCP tool — tool marked Interrupted by user, generation stops
  • Reopen session with user-only tail — auto-resumes generation
  • Reopen session with partial assistant — no auto-resume; /resume regenerates
  • SaaS mode — stream abort and reconnect via server buffer

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added session recovery controls, including a new /resume command and improved Esc behavior for interrupted chats.
    • Local sessions now support persistence and recovery, with transcript replay for active streams.
    • Added clearer assistant footers showing generation state, duration, and token usage when available.
  • Bug Fixes

    • Improved handling of interrupted streams so partially completed responses recover more reliably.
    • Better error messages are shown when chat streaming fails or resumes are unavailable.
  • Documentation

    • Updated the README in English and Chinese with the latest session recovery, shortcuts, and testing guidance.

Deliver Esc-to-interrupt with partial text preservation, tool subprocess
kill, auto-resume on user-only tail, /resume regenerate, and server-side
stream buffering. Expand .gitignore for local-only paths and document
session recovery in README.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@railway-app railway-app Bot temporarily deployed to lavish-courage / MoCode-TUI-pr-15 June 28, 2026 08:58 Destroyed
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds full session interrupt/resume support: pressing Esc kills in-flight tool subprocesses, finalizes pending tool parts, and snapshots the SSE stream into a server-side StreamReplayBuffer for replay. A PATCH /sessions/:id endpoint persists normalized transcripts; GET /chat/:id/stream serves replay streams. The CLI gains /resume to regenerate from the last user message and composer-restore logic to reinstate input text after interruption.

Changes

Session Interrupt / Resume Feature

Layer / File(s) Summary
Shared assistant content types
packages/shared/src/assistant-content.ts, packages/shared/src/index.ts
Adds AssistantMessageLike type and hasVisibleAssistantContent predicate; re-exports from the shared barrel.
CLI stream interrupt primitives and composer restore
packages/cli/src/lib/stream-interrupt.ts, packages/cli/src/lib/composer-restore.ts, packages/cli/src/lib/stream-error.ts, packages/cli/src/lib/stream-interrupt*.test.ts, packages/cli/src/lib/composer-restore.test.ts, packages/cli/src/lib/stream-error.test.ts, packages/cli/src/hooks/use-chat-autoresume.test.ts, packages/cli/src/hooks/use-chat-continue.test.ts
Adds eligibility detection (detectResumeEligibility, resolveResumeTransport, shouldAutoResumeOnMount), pending tool finalization, message normalization, auto-resume resolution, ESC composer-restore logic, and server JSON error envelope unwrapping — all with test suites.
Tool process registry and local transport active stream
packages/cli/src/lib/tool-process-registry.ts, packages/cli/src/lib/local-tools.ts, packages/cli/src/lib/local-chat-transport.ts, packages/cli/src/lib/local-chat-transport.test.ts
Adds trackToolProcess/killTrackedToolProcesses; wires tracking into the bash tool handler; implements reconnectToStream({ chatId }) backed by a new activeStream field on LocalChatTransport; adds reconnect tests.
Server SSE replay buffer and active stream registry
packages/server/src/lib/stream-buffer.ts, packages/server/src/lib/active-stream-registry.ts, packages/server/src/lib/stream-buffer.test.ts, packages/server/src/lib/active-stream-registry.test.ts, packages/server/src/routes/chat-stream.test.ts
Implements StreamReplayBuffer to ingest and replay SSE chunks; adds active-stream-registry.ts to register/clear/serve per-session buffers with user and credit gating; adds tests for all.
Server abort helpers and stream-interrupt normalizer
packages/server/src/lib/chat-abort.ts, packages/server/src/lib/stream-interrupt.ts, packages/server/src/routes/chat-abort.test.ts
Adds shouldPersistOnFinish, ingestAbortedUsageIfPresent, deprecated shouldPersistAbortedFinish, and server-side message normalization helpers; tests cover abort persistence decisions and billing stub.
Server sessions PATCH and chat route stream wiring
packages/server/src/routes/sessions.ts, packages/server/src/routes/chat.ts, packages/server/src/index.ts
Adds PATCH /:id sessions route for client-side message persistence; updates chat POST to register/ingest the SSE replay buffer, normalize interrupted messages on abort, gate persistence via shouldPersistOnFinish, and clear the active stream on error/finish; GET /:id/stream returns replay streams with credit/ownership gating.
useChat hook interrupt, persist, and resume wiring
packages/cli/src/hooks/use-chat.ts, packages/cli/src/hooks/use-chat-persist.ts, packages/cli/src/hooks/use-chat-persist.test.ts
Rewrites useChat to track turnInterrupted, skip late tool outputs, kill tracked processes, finalize interrupted assistant messages, debounce local persistence via scheduleLocalSessionPersist, wrap transport fetch errors, and expose continueGeneration/resumeStream/abort in the returned API.
Local write approval gate
packages/cli/src/lib/local-write-approval.ts, packages/cli/src/lib/local-write-approval-ui.ts, packages/cli/src/lib/local-write-approval.test.ts
Adds requiresLocalWriteApproval for writeFile/editFile in BUILD mode and a requestLocalWriteApproval dialog UI; tests cover mode/tool/allowlist combinations.
SessionChatActions provider and /resume slash command
packages/cli/src/providers/session-chat-actions.tsx, packages/cli/src/components/command-menu/commands.tsx, packages/cli/src/components/command-menu/types.ts
Adds SessionChatActionsProvider and module-level accessor; adds model to CommandContext; wires /resume command to resolve transport eligibility and call continueGeneration.
Session screen ESC interrupt, auto-resume, and scroll orchestration
packages/cli/src/screens/session.tsx, packages/cli/src/utils/list-scroll-nav.ts, packages/cli/src/components/dialogs/sessions-dialog.tsx
Refactors SessionChat to register session actions, auto-resume on mount, handle ESC interrupt with composer restore, and scroll transcript after layout; wraps render in SessionChatActionsProvider; adds local-mode sessions list path.
UI components: SessionShell, InputBar, BotMessage footer
packages/cli/src/components/session-shell.tsx, packages/cli/src/components/input-bar.tsx, packages/cli/src/components/messages/bot-message.tsx, packages/cli/src/lib/bot-message-footer.ts, packages/cli/src/lib/bot-message-footer.test.ts
Adds transcriptScrollRef and composer restore props to SessionShell/InputBar; adds useEffect to reinstate textarea text; adds shouldShowGeneratingInFooter, shouldShowDurationInFooter, and formatAssistantFooter; rewires BotMessage footer to predicate-driven generating/duration/token-usage rendering with tests.
Docs, dev tooling, and config
README.md, README.zh-CN.md, AGENTS.md, .cursor/rules/karpathy-behavioral-guidelines.mdc, .gitignore
Updates README with session recovery scenarios and /resume docs; adds Karpathy coding guidelines to .cursor/rules and AGENTS.md; expands .gitignore for local artifacts.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SessionChat
  participant useChat
  participant LocalChatTransport
  participant ServerChatRoute
  participant StreamReplayBuffer

  User->>SessionChat: press Esc
  SessionChat->>useChat: interrupt()
  useChat->>useChat: killTrackedToolProcesses()
  useChat->>useChat: finalizeInterruptedAssistant(messages)
  useChat->>ServerChatRoute: chat.stop() (abort signal)
  ServerChatRoute->>StreamReplayBuffer: clearActiveStream(buffer)
  ServerChatRoute->>ServerChatRoute: normalizeInterruptedMessages()
  ServerChatRoute->>ServerChatRoute: shouldPersistOnFinish() → db.session.update
  useChat->>ServerChatRoute: PATCH /sessions/:id (pruned messages)
  SessionChat->>SessionChat: resolvePreResponseEsc() → restore composer text

  User->>SessionChat: /resume command
  SessionChat->>useChat: continueGeneration(mode, model)
  useChat->>useChat: trimMessagesForRegenerate()
  useChat->>ServerChatRoute: POST /chat (regenerate from last user msg)
  ServerChatRoute->>StreamReplayBuffer: registerStreamBuffer + ingest(sseStream)
  StreamReplayBuffer-->>SessionChat: SSE chunks via createReplayStream()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • moyunzero/MoCode-TUI#10: Modifies packages/server/src/routes/chat.ts for Polar credits ingestion and credits gating — the same route this PR extensively rewrites for SSE replay buffering and abort persistence.
  • moyunzero/MoCode-TUI#11: Introduced the @ai-sdk/react UIMessage-based chat plumbing in packages/cli/src/hooks/use-chat.ts that this PR extends with interruption, resume, and persistence logic.
  • moyunzero/MoCode-TUI#14: Laid the MCP/BYOK tool-execution groundwork in use-chat.ts that this PR builds on to add interrupt/skip-output gating for MCP tool calls.

Poem

🐇 Hop! I pressed Escape mid-stream one day,
My tools were slain, my transcript saved away.
A buffer caught each SSE byte,
/resume brought it back to life — delight!
The composer blinked with words restored,
No session lost, no message ignored. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main scope: stream interrupts, session recovery, and resume behavior.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsd/phase-03-stream-reliability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@railway-app

railway-app Bot commented Jun 28, 2026

Copy link
Copy Markdown

🚅 Deployed to the MoCode-TUI-pr-15 environment in lavish-courage

Service Status Web Updated (UTC)
@mocode/server ✅ Success (View Logs) Web Jun 28, 2026 at 9:21 am

@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: 12

Caution

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

⚠️ Outside diff range comments (2)
packages/cli/src/lib/local-chat-transport.ts (1)

88-99: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Key the active stream by chatId.

sendMessages() accepts chatId, but Line 212 discards it and reconnectToStream() returns whichever stream last wrote the singleton slot. If this transport instance is reused across sessions, reconnecting session B can attach to session A's stream, and A finishing can also clear B's active stream. Please store { chatId, stream } (or a map) and only clear/return the matching entry.

Proposed fix
-  private activeStream: ReadableStream<UIMessageChunk> | null = null;
+  private activeStream:
+    | { chatId: string | undefined; stream: ReadableStream<UIMessageChunk> }
+    | null = null;
@@
-    const stream = new ReadableStream<UIMessageChunk>({
+    const stream = new ReadableStream<UIMessageChunk>({
       start(controller) {
         const reader = rawStream.getReader();
         void (async () => {
           try {
             while (true) {
               const { done, value } = await reader.read();
               if (done) {
-                self.activeStream = null;
+                if (self.activeStream?.stream === stream) self.activeStream = null;
                 controller.close();
                 return;
               }
               controller.enqueue(value);
             }
           } catch (error) {
-            self.activeStream = null;
+            if (self.activeStream?.stream === stream) self.activeStream = null;
             controller.error(error);
           }
         })();
       },
       cancel(reason) {
-        self.activeStream = null;
+        if (self.activeStream?.stream === stream) self.activeStream = null;
         return rawStream.cancel(reason);
       },
     });
 
-    this.activeStream = stream;
+    this.activeStream = { chatId, stream };
@@
       () => {
-        this.activeStream = null;
+        if (this.activeStream?.stream === stream) this.activeStream = null;
       },
       { once: true },
     );
 
-    void chatId;
     return stream;
   }
@@
-  async reconnectToStream(): Promise<ReadableStream<UIMessageChunk> | null> {
-    return this.activeStream;
+  async reconnectToStream({
+    chatId,
+  }: {
+    chatId: string;
+  }): Promise<ReadableStream<UIMessageChunk> | null> {
+    return this.activeStream?.chatId === chatId ? this.activeStream.stream : null;
   }
 }

Also applies to: 166-220

🤖 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 `@packages/cli/src/lib/local-chat-transport.ts` around lines 88 - 99, The
active stream handling in LocalChatTransport is using a single shared slot,
which can mix up reconnects between different chats. Update sendMessages() and
reconnectToStream() to key the stored stream by chatId (for example, keep a {
chatId, stream } entry or a map) so only the matching session is returned or
cleared. Make sure the logic in LocalChatTransport uses the chatId argument to
isolate streams per conversation and prevent one session from overwriting
another.
packages/server/src/routes/chat.ts (1)

221-254: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the active replay entry when finish persistence fails.

If db.session.update(...) throws here, the catch logs and exits without clearActiveStream(id). getActiveStreamResponse() will keep serving that registered buffer from the process-wide registry, so a failed finish leaves stale replay state and leaked buffer memory behind until another request overwrites the same session.

Proposed fix
         async onFinish(event) {
           try {
             let messagesToPersist = event.messages;
@@
           } catch (error) {
+            clearActiveStream(id);
             console.error("Failed to persist chat finish", { error, sessionId: id });
           }
         },
🤖 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 `@packages/server/src/routes/chat.ts` around lines 221 - 254, The
finish-handling path in chat.ts leaves stale replay state when db.session.update
fails because clearActiveStream(id) only runs after the update succeeds. Update
the try/catch around the session persistence logic so clearActiveStream(id) is
also executed in the failure path for the same session id, ensuring the active
stream registry is cleaned up even when “Failed to persist chat finish” is
logged. Use the existing clearActiveStream, db.session.update, and the
surrounding try/catch in the chat route to place the cleanup so it always runs
for this finish flow.
🧹 Nitpick comments (1)
packages/cli/src/screens/new-session.tsx (1)

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

Remove this drive-by formatting fix.

This whitespace change violates the Karpathy behavioral guidelines added in this PR: "Touch only code required by the request. Do not 'improve' adjacent code, comments, or formatting" and "no drive-by refactors." Revert to keep the diff strictly scoped to session interrupt/resume.

🤖 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 `@packages/cli/src/screens/new-session.tsx` at line 96, Revert the unrelated
formatting-only change in the new-session screen so the diff stays scoped to the
session interrupt/resume work. Specifically, restore the original single-line
style for the conditional around state handling in new-session.tsx and avoid any
non-functional formatting edits in the new-session component.

Source: Learnings

🤖 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 `@packages/cli/src/hooks/use-chat.ts`:
- Around line 342-359: The useEffect in use-chat.ts is re-triggering local
session persistence on every rerender because it depends on
options?.onPersistError even though that callback is only needed inside the
persist error path. Move the error handler used by scheduleLocalSessionPersist
into a ref, or memoize the callback in session.tsx where it is passed inline, so
chat.status === "ready" rerenders do not repeatedly call persistFn() and rewrite
the session file.
- Around line 244-246: The late-output skip guard in use-chat.ts is missing the
bash rejection path, so a rejected `requestBashApproval()` can still call
`chat.addToolOutput(...)` after Esc adds the tool call id to
`skipToolOutputIdsRef`. Update the `verdict === "reject"` branch to check
`shouldSkipToolOutput()` (or the same skip-set condition) before adding tool
output, matching the existing MCP and post-execution local output handling. Keep
the fix aligned with the existing `shouldSkipToolOutput`,
`skipToolOutputIdsRef`, and `chat.addToolOutput` flow so interrupted assistant
turns are not mutated.
- Around line 416-424: The `/resume` trimming logic in `use-chat` is rewriting
metadata for every retained message via the `map()` over `trimmed`, which
mutates historical mode/model provenance. Update the logic around `userIndex`
and `trimmed` so only the last retained user message that is being regenerated
gets `params.mode` and `params.model`, and leave earlier messages’ metadata
unchanged.

In `@packages/cli/src/lib/bot-message-footer.test.ts`:
- Around line 36-45: The usage assertion in the bot message footer test is too
loose because the regex in formatAssistantFooter only requires one token count
to appear. Update the test case for formatAssistantFooter so it verifies all
three usage values from usage (inputTokens, outputTokens, totalTokens) are
rendered, either by asserting each value separately or by matching the exact
footer fragment, using the existing test name that checks usage token counts
when present.

In `@packages/cli/src/lib/bot-message-footer.ts`:
- Around line 41-45: Suppress token counts in formatAssistantFooter() while a
message is still streaming so footer text matches bot-message.tsx. Update the
footer-building logic in bot-message-footer.ts to only append usage.inputTokens
and usage.outputTokens when the message is complete, using the existing
params/usage handling in formatAssistantFooter() to gate this behavior for
in-flight assistant messages.

In `@packages/cli/src/lib/local-chat-transport.test.ts`:
- Around line 120-192: The reconnect tests only verify the happy path with the
same chatId that created the stream, so they don’t prove the chatId parameter is
honored. Update the LocalChatTransport reconnect coverage by adding an assertion
in reconnectToStream that a mismatched chatId does not return the active stream
once session tracking is in place, and keep the existing
sendMessages/reconnectToStream tests aligned with the chatId-based session
contract.

In `@packages/cli/src/lib/stream-interrupt.ts`:
- Around line 83-96: The resume check in detectResumeEligibility is too broad
because any assistant tail with visible content is treated as resumable. Update
detectResumeEligibility to require an explicit interrupted/incomplete signal
before returning "partial-assistant" instead of relying on
hasVisibleAssistantContent(last) alone, and keep the "none" path for completed
assistant replies. If completed assistant tails cannot occur here, document that
invariant near detectResumeEligibility or the surrounding resume logic.

In `@packages/cli/src/screens/session.tsx`:
- Around line 135-148: Auto-resume in the session screen is reading eligibility
from raw initialMessages and then resuming with the current mode/model, which
can miss normalized recovered turns and replay them with the wrong submission
metadata. Update the useEffect flow to derive the recovered turn from the
normalized chat state/source of truth used by useChat, and pass the original
last-user-message resume metadata into continueGeneration rather than the live
prompt config. Use the existing detectResumeEligibility,
shouldAutoResumeOnMount, and continueGeneration paths to keep the resume
decision and regenerated turn aligned.

In `@packages/server/src/lib/active-stream-registry.ts`:
- Around line 10-20: `clearActiveStream()` currently deletes by `sessionId`
only, so an older request can clear a newer stream buffer in
`active-stream-registry`. Update `clearActiveStream` to accept the registered
`StreamReplayBuffer` (or equivalent request identity) and only remove the entry
if it still matches the current `activeStreams` value for that session. Then
update the finish/error cleanup call sites that use `registerStreamBuffer()` so
they pass the same buffer they registered, keeping `GET /:id/stream` scoped to
the correct active stream.

In `@packages/server/src/lib/stream-buffer.test.ts`:
- Around line 27-31: The stream-buffer tests are using setTimeout-based sleeps
to guess when ingestion has reached a certain point, which makes the replay
assertions nondeterministic. Update the affected cases in stream-buffer.test.ts
to use an explicit synchronization signal from the source stream instead of
waiting, so createReplayStream is called only after the intended chunk has been
buffered. Reuse the existing source/gate pattern in the test setup and adjust
the assertions around buffer.ingest and createReplayStream accordingly.

In `@packages/server/src/routes/chat.ts`:
- Around line 252-264: The chat failure logs currently include raw error
objects, which can leak prompt or tool payloads into server logs. Update the
error handling in the chat route’s streaming/persistence callbacks and the outer
catch around the chat handler to log only a sanitized shape, using the existing
sessionId context and a safe message/metadata extracted from the error rather
than the full error object. Keep the onError return value unchanged, but replace
any console.error usage that passes the raw error with a redacted summary in the
chat handler flow.

In `@packages/server/src/routes/sessions.ts`:
- Around line 14-16: The updateSessionSchema in sessions.ts currently accepts
any JSON array for messages, which can store malformed transcripts that later
break chat resume/submit in chat.ts. Tighten the PATCH validation by reusing the
same UI message validator used by the chat submit flow (the one feeding
validateUIMessages and MocodeUIMessage[]) so only valid persisted session
messages reach Prisma in the session update path.

---

Outside diff comments:
In `@packages/cli/src/lib/local-chat-transport.ts`:
- Around line 88-99: The active stream handling in LocalChatTransport is using a
single shared slot, which can mix up reconnects between different chats. Update
sendMessages() and reconnectToStream() to key the stored stream by chatId (for
example, keep a { chatId, stream } entry or a map) so only the matching session
is returned or cleared. Make sure the logic in LocalChatTransport uses the
chatId argument to isolate streams per conversation and prevent one session from
overwriting another.

In `@packages/server/src/routes/chat.ts`:
- Around line 221-254: The finish-handling path in chat.ts leaves stale replay
state when db.session.update fails because clearActiveStream(id) only runs after
the update succeeds. Update the try/catch around the session persistence logic
so clearActiveStream(id) is also executed in the failure path for the same
session id, ensuring the active stream registry is cleaned up even when “Failed
to persist chat finish” is logged. Use the existing clearActiveStream,
db.session.update, and the surrounding try/catch in the chat route to place the
cleanup so it always runs for this finish flow.

---

Nitpick comments:
In `@packages/cli/src/screens/new-session.tsx`:
- Line 96: Revert the unrelated formatting-only change in the new-session screen
so the diff stays scoped to the session interrupt/resume work. Specifically,
restore the original single-line style for the conditional around state handling
in new-session.tsx and avoid any non-functional formatting edits in the
new-session component.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e123a36-353d-4bde-9da4-9521178e5145

📥 Commits

Reviewing files that changed from the base of the PR and between b0d9760 and 9413260.

📒 Files selected for processing (44)
  • .cursor/rules/karpathy-behavioral-guidelines.mdc
  • .gitignore
  • AGENTS.md
  • README.md
  • README.zh-CN.md
  • packages/cli/src/components/command-menu/commands.tsx
  • packages/cli/src/components/command-menu/types.ts
  • packages/cli/src/components/dialogs/sessions-dialog.tsx
  • packages/cli/src/components/input-bar.tsx
  • packages/cli/src/components/messages/bot-message.tsx
  • packages/cli/src/components/session-shell.tsx
  • packages/cli/src/hooks/use-chat-autoresume.test.ts
  • packages/cli/src/hooks/use-chat-continue.test.ts
  • packages/cli/src/hooks/use-chat-persist.test.ts
  • packages/cli/src/hooks/use-chat-persist.ts
  • packages/cli/src/hooks/use-chat.ts
  • packages/cli/src/lib/bot-message-footer.test.ts
  • packages/cli/src/lib/bot-message-footer.ts
  • packages/cli/src/lib/composer-restore.test.ts
  • packages/cli/src/lib/composer-restore.ts
  • packages/cli/src/lib/local-chat-transport.test.ts
  • packages/cli/src/lib/local-chat-transport.ts
  • packages/cli/src/lib/local-tools.ts
  • packages/cli/src/lib/stream-error.test.ts
  • packages/cli/src/lib/stream-error.ts
  • packages/cli/src/lib/stream-interrupt.test.ts
  • packages/cli/src/lib/stream-interrupt.ts
  • packages/cli/src/lib/tool-process-registry.ts
  • packages/cli/src/providers/session-chat-actions.tsx
  • packages/cli/src/screens/new-session.tsx
  • packages/cli/src/screens/session.tsx
  • packages/cli/src/utils/list-scroll-nav.ts
  • packages/server/src/index.ts
  • packages/server/src/lib/active-stream-registry.ts
  • packages/server/src/lib/chat-abort.ts
  • packages/server/src/lib/stream-buffer.test.ts
  • packages/server/src/lib/stream-buffer.ts
  • packages/server/src/lib/stream-interrupt.ts
  • packages/server/src/routes/chat-abort.test.ts
  • packages/server/src/routes/chat-stream.test.ts
  • packages/server/src/routes/chat.ts
  • packages/server/src/routes/sessions.ts
  • packages/shared/src/assistant-content.ts
  • packages/shared/src/index.ts

Comment thread packages/cli/src/hooks/use-chat.ts
Comment thread packages/cli/src/hooks/use-chat.ts Outdated
Comment thread packages/cli/src/hooks/use-chat.ts Outdated
Comment on lines +36 to +45
test("includes usage token counts when present (D-21)", () => {
const footer = formatAssistantFooter({
mode: Mode.BUILD,
model: "claude-sonnet-4-6",
durationMs: 500,
streaming: false,
usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
});
expect(footer).toMatch(/100|150|50/);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten this usage assertion so it proves all counts are rendered.

/100|150|50/ succeeds when any one of those numbers is present, so this test still passes if the formatter drops two of the three token counts. Assert each expected value (or the exact footer fragment) instead.

Suggested fix
     const footer = formatAssistantFooter({
       mode: Mode.BUILD,
       model: "claude-sonnet-4-6",
       durationMs: 500,
       streaming: false,
       usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
     });
-    expect(footer).toMatch(/100|150|50/);
+    expect(footer).toContain("100");
+    expect(footer).toContain("50");
+    expect(footer).toContain("150");
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("includes usage token counts when present (D-21)", () => {
const footer = formatAssistantFooter({
mode: Mode.BUILD,
model: "claude-sonnet-4-6",
durationMs: 500,
streaming: false,
usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
});
expect(footer).toMatch(/100|150|50/);
});
test("includes usage token counts when present (D-21)", () => {
const footer = formatAssistantFooter({
mode: Mode.BUILD,
model: "claude-sonnet-4-6",
durationMs: 500,
streaming: false,
usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
});
expect(footer).toContain("100");
expect(footer).toContain("50");
expect(footer).toContain("150");
});
🤖 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 `@packages/cli/src/lib/bot-message-footer.test.ts` around lines 36 - 45, The
usage assertion in the bot message footer test is too loose because the regex in
formatAssistantFooter only requires one token count to appear. Update the test
case for formatAssistantFooter so it verifies all three usage values from usage
(inputTokens, outputTokens, totalTokens) are rendered, either by asserting each
value separately or by matching the exact footer fragment, using the existing
test name that checks usage token counts when present.

Comment thread packages/cli/src/lib/bot-message-footer.ts Outdated
Comment thread packages/cli/src/screens/session.tsx Outdated
Comment thread packages/server/src/lib/active-stream-registry.ts
Comment thread packages/server/src/lib/stream-buffer.test.ts Outdated
Comment thread packages/server/src/routes/chat.ts Outdated
Comment on lines +14 to +16
const updateSessionSchema = z.object({
messages: z.array(z.unknown()),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate persisted session messages before writing them.

messages: z.array(z.unknown()) lets any JSON array into session.messages, but packages/server/src/routes/chat.ts later casts that column back to MocodeUIMessage[] and feeds it into validateUIMessages(...). A malformed PATCH can poison the stored transcript and break the next submit/resume for that session. Reuse the same message validator as the chat submit route here before calling Prisma.

Proposed fix
 const updateSessionSchema = z.object({
-  messages: z.array(z.unknown()),
+  messages: z.array(
+    z.custom(
+      (value) =>
+        value != null &&
+        typeof value === "object" &&
+        "id" in value &&
+        "parts" in value,
+      "Invalid message payload",
+    ),
+  ),
 });

Also applies to: 96-110

🤖 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 `@packages/server/src/routes/sessions.ts` around lines 14 - 16, The
updateSessionSchema in sessions.ts currently accepts any JSON array for
messages, which can store malformed transcripts that later break chat
resume/submit in chat.ts. Tighten the PATCH validation by reusing the same UI
message validator used by the chat submit flow (the one feeding
validateUIMessages and MocodeUIMessage[]) so only valid persisted session
messages reach Prisma in the session update path.

Guard bash reject after Esc interrupt, preserve historical message metadata
on /resume, scope stream registry cleanup by buffer identity, stabilize
local persist callback ref, key LocalChatTransport streams by chatId, and
derive auto-resume from normalized chat state. Adds unit tests for each fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
@railway-app railway-app Bot temporarily deployed to lavish-courage / MoCode-TUI-pr-15 June 28, 2026 09:21 Destroyed
Native local write tools previously executed without confirmation while
bash blocklist and MCP write tools already had approval dialogs.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

Caution

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

⚠️ Outside diff range comments (2)
packages/server/src/routes/chat.ts (2)

128-130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Scope outer-catch cleanup to this request’s buffer.

clearActiveStream(id) deletes whatever stream is currently registered for the session. A failed second POST can therefore clear another in-flight stream for the same session; pass this request’s replayBuffer so the registry’s identity guard is used.

Proposed fix
       const userId = c.get("userId");
       const { id, messages, mode, model, mcpTools } = c.req.valid("json");
+      const replayBuffer = new StreamReplayBuffer();
 
       try {
@@
-      const replayBuffer = new StreamReplayBuffer();
@@
       } catch (error) {
-        clearActiveStream(id);
+        clearActiveStream(id, replayBuffer);
         const message = error instanceof Error ? error.message : "Chat request failed";

Also applies to: 172-172, 269-270

🤖 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 `@packages/server/src/routes/chat.ts` around lines 128 - 130, The outer-catch
cleanup in chat request handling is too broad because clearActiveStream(id) can
remove a newer in-flight stream for the same session. Update the error cleanup
in the chat route flow to pass this request’s replayBuffer into
clearActiveStream so the registry identity guard only clears the matching
stream. Apply the same scoped cleanup wherever the chat route’s try/catch or
teardown paths call clearActiveStream, using the existing replayBuffer variable
to locate the correct request-owned buffer.

214-223: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the replay registry before skipping persistence.

When shouldPersistOnFinish returns false, this returns without clearing the buffer registered in consumeSseStream; the pending-tool-call path can leave a stale closed stream in the active registry.

Proposed fix
           ) {
+            clearActiveStream(id, replayBuffer);
             return;
           }
🤖 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 `@packages/server/src/routes/chat.ts` around lines 214 - 223, The early return
in the `shouldPersistOnFinish` branch leaves the SSE replay buffer registered by
`consumeSseStream` in the active registry. Before returning false, make sure the
stream is removed/cleared from the replay registry so the pending-tool-call path
cannot leave a stale closed stream behind. Update the cleanup flow around the
`shouldPersistOnFinish` check in `chat.ts` to always unregister the stream
before skipping persistence.
🤖 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.

Outside diff comments:
In `@packages/server/src/routes/chat.ts`:
- Around line 128-130: The outer-catch cleanup in chat request handling is too
broad because clearActiveStream(id) can remove a newer in-flight stream for the
same session. Update the error cleanup in the chat route flow to pass this
request’s replayBuffer into clearActiveStream so the registry identity guard
only clears the matching stream. Apply the same scoped cleanup wherever the chat
route’s try/catch or teardown paths call clearActiveStream, using the existing
replayBuffer variable to locate the correct request-owned buffer.
- Around line 214-223: The early return in the `shouldPersistOnFinish` branch
leaves the SSE replay buffer registered by `consumeSseStream` in the active
registry. Before returning false, make sure the stream is removed/cleared from
the replay registry so the pending-tool-call path cannot leave a stale closed
stream behind. Update the cleanup flow around the `shouldPersistOnFinish` check
in `chat.ts` to always unregister the stream before skipping persistence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3de3f8e4-c72b-4936-bc95-a7364ac61703

📥 Commits

Reviewing files that changed from the base of the PR and between 9413260 and 8c3717b.

📒 Files selected for processing (17)
  • packages/cli/src/hooks/use-chat-persist.test.ts
  • packages/cli/src/hooks/use-chat.ts
  • packages/cli/src/lib/bot-message-footer.test.ts
  • packages/cli/src/lib/bot-message-footer.ts
  • packages/cli/src/lib/local-chat-transport.test.ts
  • packages/cli/src/lib/local-chat-transport.ts
  • packages/cli/src/lib/local-write-approval-ui.ts
  • packages/cli/src/lib/local-write-approval.test.ts
  • packages/cli/src/lib/local-write-approval.ts
  • packages/cli/src/lib/stream-interrupt-resume.test.ts
  • packages/cli/src/lib/stream-interrupt.ts
  • packages/cli/src/screens/session.tsx
  • packages/server/src/lib/active-stream-registry.test.ts
  • packages/server/src/lib/active-stream-registry.ts
  • packages/server/src/lib/stream-buffer.test.ts
  • packages/server/src/lib/stream-buffer.ts
  • packages/server/src/routes/chat.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/src/lib/local-write-approval.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/cli/src/hooks/use-chat-persist.test.ts
  • packages/cli/src/lib/bot-message-footer.ts
  • packages/server/src/lib/stream-buffer.ts
  • packages/cli/src/lib/bot-message-footer.test.ts
  • packages/server/src/lib/stream-buffer.test.ts
  • packages/cli/src/lib/local-chat-transport.ts
  • packages/cli/src/screens/session.tsx
  • packages/cli/src/hooks/use-chat.ts

@moyunzero moyunzero merged commit 42e753a into master Jun 28, 2026
2 checks passed
@railway-app railway-app Bot temporarily deployed to lavish-courage / MoCode-TUI-pr-15 June 28, 2026 10:57 Destroyed
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