Skip to content

feat(desktop): show a restored-session separator when a v2 terminal cold-restores - #5448

Merged
Kitenite merged 4 commits into
mainfrom
terminal-cold-restore-mes
Jul 4, 2026
Merged

Kitenite merged 4 commits into
mainfrom
terminal-cold-restore-mes

Conversation

@Kitenite

@Kitenite Kitenite commented Jul 3, 2026 •

Copy link
Copy Markdown
Member

Summary

  • When host-service respawns a lost terminal session (reboot, pty-daemon loss), it now injects a dim ─── Session Contents Restored ─── separator into the replay buffer ahead of the new shell's first output — so users can tell restored scrollback from the fresh session instead of a new prompt silently appearing under dead content.

Why / Context

In v2, a cold restore is host-side: the renderer paints its persisted scrollback, then the WS attach finds an active session row whose PTY the daemon no longer owns, and host-service respawns a fresh shell (packages/host-service/src/terminal/terminal.ts WS route fallback). Previously nothing marked the boundary — users saw their old content with a brand-new prompt below and no hint the session had died. Other terminal apps mark this (VS Code's "History restored", iTerm2's session-restored banner).

How It Works

  • New restoredNotice option on createTerminalSessionInternal, set only by the WS respawn fallback.
  • The separator bytes are appended to the session's replay buffer immediately after session registration — the buffer is empty at that point (daemon subscribe hasn't started), so the notice is guaranteed to precede the new shell's first output on replay.
  • Injecting inside createTerminalSessionInternal (not at the call site) dedupes concurrent attaches racing into the respawn path, and the !isAdopted guard suppresses the notice when the daemon still owns a live shell (host-service restart adoption) — nothing died, so no separator.
  • Host-side injection means any client (desktop renderer, future web) gets it with no renderer changes, and it persists into xterm scrollback/localStorage like ordinary output.

Manual QA Checklist

Verified end-to-end in the dev desktop app over CDP:

  • Cold restore: killed host-service + the terminal's shell, reloaded the renderer → reattach respawned the shell and the dim separator rendered between the restored scrollback (marker output intact) and the fresh prompt
  • Adoption path: reattaching to a still-live daemon session shows no separator
  • Fresh terminals (POST /terminal/sessions) unaffected — no separator

Testing

  • bun run lint (clean)
  • bun run typecheck for @superset/host-service (clean)
  • node --experimental-strip-types --test src/terminal/terminal.adoption.node-test.ts — 17/17 pass, including two new tests: separator is first in the replay buffer on a respawn; absent on adoption. Both were mutation-verified (breaking the guard or removing the injection fails the corresponding test). Note: these run under node ABI; a node_modules currently rebuilt for Electron will ERR_DLOPEN until rebuilt for node.

Open in Stage

Summary by cubic

Add a dim "─── Session Contents Restored ───" separator on v2 terminal cold-restore so users see where restored scrollback ends and the new shell begins. Skipped when adopting a still-live daemon session; survives buffer eviction and closed-socket attaches.

  • New Features

    • Host-service prepends the notice on the first replay, after the mode preamble and before the new shell’s output.
    • Added restoredNotice to createTerminalSessionInternal; set only by the WS respawn path and ignored when isAdopted is true.
    • Injected server-side so all clients get it, and it persists in scrollback.
  • Bug Fixes

    • Kept the separator out of the bounded replay FIFO; replayBuffer now prepends it on first attach so large output bursts can’t evict it.
    • Ensured the separator appears only on the first replay; added tests for flood and adoption, and cleaned up redundant test buffer copies.
    • Avoided losing the notice when an attach socket is already closed; replayBuffer now bails early instead of clearing replay state.

Written for commit 5595d16. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a “Session Contents Restored” separator to terminal scrollback during cold restores/respawns, inserted before the recreated session’s replayed output.
  • Bug Fixes
    • Ensured the separator appears only once per restore, remains ahead of any flooded output, and is omitted when attaching to an already-active adopted session.
  • Tests
    • Added end-to-end coverage for restored-notice ordering and presence/absence across restore scenarios.

…old-restores

When the pty-daemon no longer owns a terminal's PTY (reboot, daemon loss),
host-service silently respawned a fresh shell under the renderer's restored
scrollback — a new prompt just appeared with no indication the session died.
Inject a dim "─── Session Contents Restored ───" separator into the replay
buffer ahead of the respawned shell's first output, so restored content and
the fresh session are visibly distinct (like VS Code's "History restored").

Skipped when the daemon session is adopted (shell still alive across a
host-service restart) — nothing died, so no separator.
@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a SESSION_RESTORED_NOTICE separator to terminal replay output for cold-restore respawns, gated by a new restoredNotice option and cleared after first replay. The test suite adds coverage for fresh respawns, buffered-output eviction, and adopted live sessions.

Changes

Session Restored Notice

Layer / File(s) Summary
Restored notice replay path
packages/host-service/src/terminal/terminal.ts
Adds SESSION_RESTORED_NOTICE, tracks restoredNoticePending on session state, prepends the notice before buffered replay bytes, clears it after replay, extends session creation options, and passes restoredNotice: true on the lost-session respawn path.
Replay assertions for restored notice
packages/host-service/src/terminal/terminal.adoption.node-test.ts
Adds a replay capture helper and three end-to-end tests covering first-replay emission, FIFO eviction ordering, and omission for adopted live sessions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a restored-session separator for v2 terminal cold restores.
Description check ✅ Passed The description is detailed and covers summary, context, implementation, manual QA, and testing, though some template sections are omitted.
✨ 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 terminal-cold-restore-mes

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.

@ghost

ghost commented Jul 3, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 5 individual chapters for you:

Title
1 Define the session restoration notice and state
2 Inject the notice into the replay buffer
3 Wire restoration options through session creation
4 Enable the notice on WebSocket respawn
5 Test session restoration and notice delivery
Open in Stage

Chapters generated by Stage for commit 5595d16 on Jul 4, 2026 12:21am UTC.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/host-service/src/terminal/terminal.ts`:
- Around line 1092-1097: The restored notice in terminal buffering can be
evicted by FIFO trimming in bufferOutput() before clients replay it. Update the
restored-notice path in terminal.ts so SESSION_RESTORED_NOTICE is protected from
MAX_BUFFER_BYTES eviction, either by reserving space for it or excluding it from
the oldest-chunk removal logic, and make sure the behavior is handled in the
restoredNotice/isAdopted branch that calls bufferOutput().
🪄 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

Run ID: f5010363-1fef-46e2-9a9d-34da0abecded

📥 Commits

Reviewing files that changed from the base of the PR and between 58a759d and 25e1065.

📒 Files selected for processing (2)
  • packages/host-service/src/terminal/terminal.adoption.node-test.ts
  • packages/host-service/src/terminal/terminal.ts

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR injects a dim ─── Session Contents Restored ─── ANSI separator into a v2 terminal's replay buffer when host-service respawns a dead session (cold restore), so users can distinguish persisted scrollback from the new shell's output. The injection is synchronous, buffer-first (before daemon.subscribe starts), and correctly suppressed when the daemon has already adopted a live shell.

  • terminal.ts: Adds a module-level SESSION_RESTORED_NOTICE constant and a restoredNotice option to CreateTerminalSessionOptions; the option is consumed with an !isAdopted guard immediately after sessions.set, before any daemon output can arrive, ensuring the notice is always buffer[0] on a fresh spawn.
  • terminal.ts call site (WS route): restoredNotice: true is set only in the respawn fallback after adoption fails — POST /terminal/sessions and adoption paths are unaffected.
  • terminal.adoption.node-test.ts: Two new E2E tests verify the notice is first in the buffer on respawn and absent on adoption; both were mutation-verified per the PR description.

Confidence Score: 4/5

Safe to merge; the change is narrowly scoped to the WS cold-restore respawn path and does not touch adoption, fresh terminal creation, or any data path that existed before.

The injection point (synchronously after sessions.set, before daemon.subscribe) guarantees ordering. The !isAdopted guard correctly suppresses the notice for live-session adoption and the session-already-exists fallback. The only note is a minor redundant dead-code pattern in both new tests.

No files require special attention; the two new test cases cover the production paths well.

Important Files Changed

Filename Overview
packages/host-service/src/terminal/terminal.ts Adds SESSION_RESTORED_NOTICE constant and restoredNotice option to createTerminalSessionInternal; notice is injected synchronously into the empty buffer before daemon subscribe, correctly gated by !isAdopted; call site is limited to the WS cold-restore respawn path only.
packages/host-service/src/terminal/terminal.adoption.node-test.ts Two new end-to-end tests covering the separator injection (fresh spawn) and absence (adopted session); tests are mutation-verified per the PR description. Minor: redundant assert.ok + if-guard pattern in both new tests.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as Renderer (WS client)
    participant H as host-service (WS route)
    participant D as pty-daemon
    participant S as sessions Map

    R->>H: WS connect (terminalId)
    H->>H: resolveSessionForAttach()
    H->>D: "adoptOnly=true, daemon.list()"
    D-->>H: session not alive (cold restore)
    Note over H: adoption failed
    H->>D: daemon.open(terminalId, new shell)
    D-->>H: pid (new PTY)
    H->>S: sessions.set(terminalId, session)
    Note over H: restoredNotice and not isAdopted
    H->>S: bufferOutput SESSION_RESTORED_NOTICE
    Note over S: buffer[0] = separator
    H->>D: daemon.subscribe replay true
    D-->>H: onOutput shell output bufferOutput
    Note over S: buffer[1..n] = new shell output
    H-->>R: attachSocketToSession replayBuffer
    Note over R: Sees separator then new prompt
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant R as Renderer (WS client)
    participant H as host-service (WS route)
    participant D as pty-daemon
    participant S as sessions Map

    R->>H: WS connect (terminalId)
    H->>H: resolveSessionForAttach()
    H->>D: "adoptOnly=true, daemon.list()"
    D-->>H: session not alive (cold restore)
    Note over H: adoption failed
    H->>D: daemon.open(terminalId, new shell)
    D-->>H: pid (new PTY)
    H->>S: sessions.set(terminalId, session)
    Note over H: restoredNotice and not isAdopted
    H->>S: bufferOutput SESSION_RESTORED_NOTICE
    Note over S: buffer[0] = separator
    H->>D: daemon.subscribe replay true
    D-->>H: onOutput shell output bufferOutput
    Note over S: buffer[1..n] = new shell output
    H-->>R: attachSocketToSession replayBuffer
    Note over R: Sees separator then new prompt
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/host-service/src/terminal/terminal.adoption.node-test.ts:352-354
**Dead-code guard after a throwing assertion**

`assert.ok(!("error" in result))` throws when the condition is false, so the `if ("error" in result) return;` immediately below is unreachable and adds noise. The identical pattern appears in the second test (`assert.ok(!("error" in second))` then `if ("error" in second) return;`). Pick one: keep only the `assert.ok` (preferred — gives a clear failure message), or drop the `assert.ok` and rely on the type-narrowing `if`.

Reviews (1): Last reviewed commit: "feat(desktop): show a restored-session s..." | Re-trigger Greptile

Comment on lines +352 to +354
listed: true,
restoredNotice: true,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dead-code guard after a throwing assertion

assert.ok(!("error" in result)) throws when the condition is false, so the if ("error" in result) return; immediately below is unreachable and adds noise. The identical pattern appears in the second test (assert.ok(!("error" in second)) then if ("error" in second) return;). Pick one: keep only the assert.ok (preferred — gives a clear failure message), or drop the assert.ok and rely on the type-narrowing if.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/terminal.adoption.node-test.ts
Line: 352-354

Comment:
**Dead-code guard after a throwing assertion**

`assert.ok(!("error" in result))` throws when the condition is false, so the `if ("error" in result) return;` immediately below is unreachable and adds noise. The identical pattern appears in the second test (`assert.ok(!("error" in second))` then `if ("error" in second) return;`). Pick one: keep only the `assert.ok` (preferred — gives a clear failure message), or drop the `assert.ok` and rely on the type-narrowing `if`.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping as-is: assert.ok gives the failure message and the if guard does the TypeScript narrowing — this pattern is the established idiom throughout this file (19 occurrences); changing only the new tests would be inconsistent.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/terminal/terminal.ts">

<violation number="1" location="packages/host-service/src/terminal/terminal.ts:1095">
P2: The restored-session separator is inserted as the oldest buffer chunk via `bufferOutput`, but the same function evicts oldest chunks once `MAX_BUFFER_BYTES` (64 KB) is exceeded. If the newly spawned shell produces a burst of output before any client replays (e.g., a verbose shell init), the notice will be the first thing evicted and no client will ever see it.

Consider pinning the notice so it's excluded from byte-budget eviction, or re-injecting it at replay time rather than storing it in the FIFO buffer.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

…replay FIFO

Review finding (CodeRabbit/cubic): the separator was injected as the oldest
FIFO chunk, so a >64KB output burst before any client attached would evict
it first. Track it as restoredNoticePending on the session instead and let
replayBuffer prepend it (after the mode preamble) on first replay — immune
to MAX_BUFFER_BYTES eviction by construction. Adds a flood e2e test that
fails under the old FIFO-injection approach.
@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions

github-actions Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Admin (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

…ady closed

replayBuffer cleared restoredNoticePending and the FIFO before sendBytes,
which silently no-ops on a non-open socket — losing both for the next
attach. Bail early instead so a dead socket leaves replay state intact.
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@Kitenite
Kitenite merged commit b3e5928 into main Jul 4, 2026
11 checks passed
@Kitenite
Kitenite deleted the terminal-cold-restore-mes branch July 4, 2026 00:23

This branch was previously deployed

1 inactive deployment
preview — 5595d16b Deployed Jul 4, 2026 by Kitenite via Deploy Docs #6165
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