From 62cf5232c72bbf29debd1a484800fc9cca7de460 Mon Sep 17 00:00:00 2001 From: morluto Date: Wed, 1 Jul 2026 13:15:29 +0000 Subject: [PATCH] fix(agent-core): dispose session terminal records on close --- .changeset/session-terminal-records.md | 5 ++++ .../src/services/terminal/terminalService.ts | 26 +++++++++++++++++ .../test/services/terminal-service.test.ts | 28 +++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changeset/session-terminal-records.md diff --git a/.changeset/session-terminal-records.md b/.changeset/session-terminal-records.md new file mode 100644 index 000000000..6420f4447 --- /dev/null +++ b/.changeset/session-terminal-records.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Release session terminal records when sessions close. diff --git a/packages/agent-core/src/services/terminal/terminalService.ts b/packages/agent-core/src/services/terminal/terminalService.ts index def013355..6f362e473 100644 --- a/packages/agent-core/src/services/terminal/terminalService.ts +++ b/packages/agent-core/src/services/terminal/terminalService.ts @@ -60,6 +60,11 @@ export class TerminalService extends Disposable implements ITerminalService { this.defaultCols = options.defaultCols ?? DEFAULT_COLS; this.defaultRows = options.defaultRows ?? DEFAULT_ROWS; this.maxBufferedFrames = options.maxBufferedFrames ?? DEFAULT_MAX_BUFFERED_FRAMES; + this._register( + this.sessionService.onDidClose(({ sessionId }) => { + this.closeSessionRecords(sessionId); + }), + ); } async create(sessionId: string, input: CreateTerminalRequest): Promise { @@ -174,6 +179,27 @@ export class TerminalService extends Disposable implements ITerminalService { super.dispose(); } + private closeSessionRecords(sessionId: string): void { + const prefix = `${sessionId}\0`; + for (const [key, record] of Array.from(this.records.entries())) { + if (!key.startsWith(prefix)) continue; + if (!record.closed) { + record.closed = true; + try { + record.process.kill(); + } catch { + } + this.markExited(record, null); + } else { + disposeAll(record.disposables); + record.disposables = []; + } + record.sinks.clear(); + record.buffer = []; + this.records.delete(key); + } + } + private async requireRecord( sessionId: string, terminalId: string, diff --git a/packages/agent-core/test/services/terminal-service.test.ts b/packages/agent-core/test/services/terminal-service.test.ts index 131573fd7..23a507a70 100644 --- a/packages/agent-core/test/services/terminal-service.test.ts +++ b/packages/agent-core/test/services/terminal-service.test.ts @@ -111,7 +111,10 @@ function session(id: string, cwd: string): Session { }; } -function makeSessionService(sessions: Map): ISessionService { +function makeSessionService( + sessions: Map, + closeEmitter = new Emitter<{ sessionId: string }>(), +): ISessionService { const emptyEmitter = new Emitter(); return { _serviceBrand: undefined, @@ -148,7 +151,7 @@ function makeSessionService(sessions: Map): ISessionService { throw new Error('not implemented'); }, onDidCreate: emptyEmitter.event, - onDidClose: emptyEmitter.event, + onDidClose: closeEmitter.event, }; } @@ -292,4 +295,25 @@ describe('TerminalService streams', () => { TerminalNotFoundError, ); }); + + it('kills and forgets session terminals when the session closes', async () => { + const root = join(tmpDir, 'workspace-h'); + mkdirSync(root, { recursive: true }); + const backend = new FakeTerminalBackend(); + const closeEmitter = new Emitter<{ sessionId: string }>(); + const svc = new TerminalService({ backend }, makeSessionService(new Map([ + ['sess_h', session('sess_h', root)], + ]), closeEmitter)); + const terminal = await svc.create('sess_h', {}); + const process = backend.processes[0]!; + process.emitData('buffered output'); + + closeEmitter.fire({ sessionId: 'sess_h' }); + + expect(process.killed).toBe(true); + expect(await svc.list('sess_h')).toEqual([]); + await expect(svc.get('sess_h', terminal.id)).rejects.toBeInstanceOf( + TerminalNotFoundError, + ); + }); });