fix(agent): release parked subagent session memory on dispose

Keep-alive subagents are handed to AgentLifecycleManager.adopt, which stores
their reviver closure in the process-global #adopted map. The closure is
defined inside runSubagent's scope, which also captures the live AgentSession
(extension-runner callbacks, buildSubagentSessionOptions), so its lexical
environment pins the whole session graph. park() disposes and detaches the
session but leaves the adoption record indefinitely, and #doDispose never
dropped the in-memory transcript, session-manager entries, or the raw-SSE
debug buffer (whose trimmed records retain slice() views of full wire frames),
so every completed subagent's heavy state leaked for the process lifetime.

dispose() is terminal and every revival path reopens from disk, so #doDispose
now sheds retained conversation memory via agent.reset(),
RawSseDebugBuffer.clear(), and SessionManager.releaseRetainedEntries(). The
adoption record can still reference the session, but only as a husk.

Fixes #8003
This commit is contained in:
roboomp
2026-08-08 09:42:35 +00:00
parent 08819b279c
commit ed6300b35e
5 changed files with 141 additions and 0 deletions
@@ -335,6 +335,26 @@ export class RawSseDebugBuffer {
return body.length > 0 ? `${dropped}${body}` : dropped;
}
/**
* Drop every retained record and reset accounting. Called from
* {@link AgentSession} teardown so a disposed (e.g. parked subagent) session
* stops pinning captured wire frames — each trimmed record holds a
* `slice()` of its parent SSE frame, which under JSC keeps the whole
* multi-MB frame alive. Notifies subscribers so a live debug viewer redraws
* empty.
*/
clear(): void {
this.#records = [];
this.#recordChars = [];
this.#head = 0;
this.#totalChars = 0;
this.#droppedRecords = 0;
this.#droppedChars = 0;
this.#totalEvents = 0;
this.#lastUpdatedAt = undefined;
this.#emit();
}
#append(record: RawSseDebugRecord, chars: number): void {
this.#records.push(record);
this.#recordChars.push(chars);