Skip to content

fix(hub): throw when restarting a session with a closed stream - #164

Open
dvcolomban wants to merge 2 commits into
devframes:mainfrom
dvcolomban:dvcol/fix-terminal-restart-closed-stream
Open

fix(hub): throw when restarting a session with a closed stream#164
dvcolomban wants to merge 2 commits into
devframes:mainfrom
dvcolomban:dvcol/fix-terminal-restart-closed-stream

Conversation

@dvcolomban

@dvcolombandvcolomban commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

restart() on a startChildProcess()/startPtySession() session silently returned once its streamClosed flag was set — after a natural exit, or after terminate(). That flag guards a single-use ReadableStream controller that can't be reopened, so restarting in that state genuinely can't work. The bug isn't the refusal, it's that it's silent: hub:terminals:restart resolves either way, so a caller can't tell "restarted" from "did nothing."

This throws a new DF8206 instead, pointing at the actual recovery: ctx.terminals.remove(session) then a fresh start*() with a new id. Covers the RPC path for free since hub:terminals:restart just awaits session.restart().

Why not DF8205

Its fix text says "it was registered with restartable: false" — that'd misdescribe a session whose stream is just spent. restartable is a different concept (lifecycle owned elsewhere) that host-terminals.ts never even mutates.

Behavior change, on purpose

host-terminals.test.ts had two tests explicitly pinning the no-op (does not restart a terminated child-process session, keeps status stopped when restart() is called after the process exited) from #148, four days ago. Both are updated here to assert the DF8206 rejection instead, keeping their original assertions (stream stays closed, status stays 'stopped') — only the silence changes. I think that's worth doing even though it revises recently-pinned behavior: an undiscoverable no-op on a public API is a footgun no matter how recently it shipped.

Blast radius looks like zero — nothing outside this repo I can find calls restart() or reads restartable.

Also added a one-line JSDoc on both public restart(): Promise<void> declarations documenting the throw — no snapshot impact, tsnapi strips JSDoc.

Tests

pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build — all green (1054 tests).

CopilotAI lite review requested due to automatic review settings August 5, 2026 09:02
@netlify

netlifyBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

NameLink
🔨 Latest commitdedffdb
🔍 Latest deploy loghttps://app.netlify.com/projects/devfra/deploys/6a7306d02a74d3000852fcf7
😎 Deploy Previewhttps://deploy-preview-164--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changesRun an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

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

Pull request overview

This PR fixes a usability bug in the hub terminals lifecycle by making restart() fail loudly when a terminal session’s output stream has already been closed (after natural exit or terminate()), which cannot be recovered in-place due to the one-shot ReadableStream controller.

Changes:

  • Throw new DF8206 from startChildProcess() / startPtySession() session restart() when streamClosed is set (instead of silently no-op’ing).
  • Add DF8206 diagnostic definition and update tests to assert the rejection behavior.
  • Document the new error in public types JSDoc and add docs/errors/DF8206.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
packages/hub/src/types/terminals.tsDocuments that restart() throws DF8206 once the stream has closed.
packages/hub/src/node/host-terminals.tsChanges restart() to throw DF8206 when the session’s output stream is already closed.
packages/hub/src/node/diagnostics.tsAdds the new DF8206 coded diagnostic with a recovery hint.
packages/hub/src/node/tests/host-terminals.test.tsUpdates assertions to expect restart() to reject with DF8206 after exit/terminate.
docs/errors/DF8206.mdAdds the error reference page for DF8206.
Suppressed comments (1)

packages/hub/src/types/terminals.ts:144

  • Same as above: remove() is ambiguous here (it is not a method on the session). Prefer pointing callers at ctx.terminals.remove(session) explicitly.
 /** Throws `DF8206` once the session's output stream has closed (after a natural exit or `terminate()`) — `remove()` it and start a fresh session instead. */
restart: () => Promise<void>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/hub/src/types/terminals.ts Outdated
Comment threadpackages/hub/src/node/diagnostics.ts
Comment threaddocs/errors/DF8206.md Outdated
restart() silently returned once a startChildProcess()/startPtySession()
session's streamClosed flag was set — after a natural process exit or
terminate(). That flag guards a single-use ReadableStream controller
which cannot be reopened, so restarting in place genuinely cannot
work; the defect was that a caller had no way to distinguish success
from a no-op (hub:terminals:restart resolves either way).
Throw a new DF8206 diagnostic instead, pointing callers at
remove(session) + a fresh start*() with a new id. This is a behaviour
change on a case devframes#148 (four days ago) deliberately pinned as a silent
no-op — the two host-terminals tests that pinned it are updated to
assert the rejection while keeping their original assertions (stream
stays closed / status stays 'stopped'): only the silence changes.
Not reusing DF8205 (its fix text describes restartable: false, which
would misdescribe a spent stream) and not flipping `restartable` in
closeStream() (a different concept — "lifecycle owned elsewhere" vs.
"stream spent").
🤖 Generated with [Claude Code](https://claude.com/claude-code)
@dvcolomban
dvcolombanforce-pushed the dvcol/fix-terminal-restart-closed-stream branch from 124e909 to e56dd62CompareAugust 5, 2026 09:29
Address review feedback on the wording only, no behaviour change:
remove() is a method on ctx.terminals, not on the session, so both
restart() JSDoc lines said it wrongly; DF8206's fix text now prefixes
the calls with ctx.terminals. the way DF8202/DF8204 do and spells out
the two recovery steps instead of contracting them; the docs page's
Fix section is reworded as a sentence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
CopilotAI review requested due to automatic review settings August 5, 2026 09:47

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Sign up for freeto 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.

2 participants

@dvcolomban