Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/hub/src/node/__tests__/host-terminals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,42 @@ describe('devframeTerminalHost stream lifecycle', () => {
})
expect(session.getChildProcess()).toBeUndefined()
})

it('bounds the session scrollback buffer', async () => {
const { host } = createTerminalHost()
let controller: ReadableStreamDefaultController<string>
const stream = new ReadableStream<string>({
start(_controller) {
controller = _controller
},
})
const session: DevframeTerminalSession = { id: 't', title: 'T', status: 'running', stream }
host.register(session)
for (let i = 0; i < 1200; i++) controller!.enqueue(`line-${i}`)
// Wait for the read loop to drain every enqueued chunk (each read is its
// own microtask tick) before asserting the trimmed shape.
await waitUntil(() => {
expect(session.buffer!.at(-1)).toBe('line-1199')
})
// Keeps the newest, drops the oldest.
expect(session.buffer!.length).toBeLessThanOrEqual(1000)
expect(session.buffer!.includes('line-0')).toBe(false)
})

it('does not restart a terminated child-process session', async () => {
const { host, sinks } = createTerminalHost()
const session = await host.startChildProcess(
{ command: process.execPath, args: ['-e', 'setInterval(() => {}, 1000)'] },
{ id: 'child', title: 'Child' },
)
await session.terminate()
await waitUntil(() => {
expect(sinks.get('child')?.closed).toBe(true)
})
await session.restart()
// Stream stays closed; no orphan output stream.
expect(sinks.get('child')?.closed).toBe(true)
})
})

describe('devframeTerminalHost interactive PTY sessions', () => {
Expand Down
10 changes: 9 additions & 1 deletion packages/hub/src/node/host-terminals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type PartialWithoutId<T extends { id: string }> = Partial<T> & { id: string }
*/
const TERMINAL_STREAM_CHANNEL = 'devframe:terminals' as const
const TERMINAL_REPLAY_WINDOW = 1000
/** Max chunks retained in the per-session scrollback buffer (bounded like the replay window). */
const TERMINAL_BUFFER_LIMIT = 1000

/** TERM handed to spawned PTYs; also used to reject fallback process labels. */
const PTY_TERM_NAME = 'xterm-256color'
Expand Down Expand Up @@ -123,8 +125,10 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
if (result.done)
break
// Mirror to the legacy session.buffer used by `terminals:read` —
// unbounded history kept for the snapshot endpoint.
// bounded tail kept for the snapshot endpoint.
sessionBuffer.push(result.value)
if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT)
sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT)
sink?.write(result.value)
}
if (!disposed && sink && !sink.closed)
Expand Down Expand Up @@ -243,6 +247,8 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
cp = createChildProcess()

const restart = async () => {
if (streamClosed)
return
cp?.kill()
cp = createChildProcess()
}
Expand Down Expand Up @@ -394,6 +400,8 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
closeStream()
},
restart: async () => {
if (streamClosed)
return
pty?.kill()
pty = spawnPty()
},
Expand Down
207 changes: 0 additions & 207 deletions plans/008-cap-terminal-buffer.md

This file was deleted.

2 changes: 1 addition & 1 deletion plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ changes are allowed as long as they're marked).
| 005 | Don't cache a rejected RPC `setup()` promise | bug | P1 | S | — | DONE |
| 006 | Bound `SharedState.syncIds` (memory leak) | bug | P1 | S | — | DONE |
| 007 | Behavioral tests for the auth/OTP trust boundary | tests | P1 | S | — | DONE |
| 008 | Cap hub terminal buffer + reject restart-after-terminate | bug | P2 | S | — | TODO |
| 008 | Cap hub terminal buffer + reject restart-after-terminate | bug | P2 | S | — | DONE |
| 009 | Fix two server-side streaming lifecycle leaks | bug | P2 | S-M | — | TODO |
| 010 | Initialize client shared state once across trust flips | bug | P2 | S | — | TODO |
| 011 | Don't leak/drop when WS client posts on a closing socket | bug | P2 | S | — | DONE |
Expand Down
Loading