fix(typescript/agent-os-vscode): release sreServer exit listeners on whichever event fires first#2176
Merged
imran-siddique merged 1 commit intoMay 12, 2026
Conversation
…whichever event fires first
`SREServerManager.start` was registering plain `.on('error', ...)` and
`.on('exit', ...)` handlers on the spawned subprocess:
this._proc.on('error', () => { this._proc = undefined; });
this._proc.on('exit', () => { this._proc = undefined; });
Node's `child_process` can emit either path: `'error'` then `'exit'`
(typical for spawn failures like ENOENT), or `'exit'` alone. With `.on`,
both handlers stay attached on the dead ChildProcess forever; the orphan
listener pins its closure (and anything it captures) until the
ChildProcess itself is collected. Swapping to `.once()` would auto-
remove the fired handler but leave the sibling waiting forever for an
event that may never come.
Extract a `wireExitListeners(proc, onTerminated)` helper that registers
both with `.once()` AND has each handler proactively remove the other
on first fire. The `onTerminated` callback is debounced so that `error`
followed by `exit` invokes it exactly once. The helper accepts a
narrow `ProcessExitTarget` contract (just `.once` and `.removeListener`)
so the cleanup behaviour is testable with a plain `EventEmitter`.
Add unit tests in the existing `sreServer.test.ts` exercising all three
firing orders (error-only, exit-only, error-then-exit) and asserting
both that the callback fires exactly once AND that the listener counts
on the emitter drop to zero after the first event.
🤖 AI Agent: security-scanner — View detailsNo security issues found. |
🤖 AI Agent: test-generator — `sreServer.ts`
|
🤖 AI Agent: code-reviewer — View detailsTL;DR: 0 blockers, 0 warnings. No issues found. Clean change. |
🤖 AI Agent: docs-sync-checker — Docs SyncDocs Sync
|
🤖 AI Agent: breaking-change-detector — API CompatibilityAPI Compatibility
|
|
🟡 Contributor Check: MEDIUM
Automated check by AGT Contributor Check. |
PR Review Summary
Verdict: ✅ Ready for human review |
MohammadHaroonAbuomar
pushed a commit
to MohammadHaroonAbuomar/agt-acs
that referenced
this pull request
Jun 1, 2026
…whichever event fires first (microsoft#2176) `SREServerManager.start` was registering plain `.on('error', ...)` and `.on('exit', ...)` handlers on the spawned subprocess: this._proc.on('error', () => { this._proc = undefined; }); this._proc.on('exit', () => { this._proc = undefined; }); Node's `child_process` can emit either path: `'error'` then `'exit'` (typical for spawn failures like ENOENT), or `'exit'` alone. With `.on`, both handlers stay attached on the dead ChildProcess forever; the orphan listener pins its closure (and anything it captures) until the ChildProcess itself is collected. Swapping to `.once()` would auto- remove the fired handler but leave the sibling waiting forever for an event that may never come. Extract a `wireExitListeners(proc, onTerminated)` helper that registers both with `.once()` AND has each handler proactively remove the other on first fire. The `onTerminated` callback is debounced so that `error` followed by `exit` invokes it exactly once. The helper accepts a narrow `ProcessExitTarget` contract (just `.once` and `.removeListener`) so the cleanup behaviour is testable with a plain `EventEmitter`. Add unit tests in the existing `sreServer.test.ts` exercising all three firing orders (error-only, exit-only, error-then-exit) and asserting both that the callback fires exactly once AND that the listener counts on the emitter drop to zero after the first event.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SREServerManager.startregistered plain (long-lived) listeners on the spawnedpython -m agent_failsafe.rest_serversubprocess:Node's
child_processcan drive this pair two ways:'error'then'exit'— the spawn itself failed (ENOENT, EACCES, etc.) and Node emits the error and then synthesises an exit. Both handlers fire and_proc = undefinedis set twice; that's harmless on its own, but the handlers stay registered on the deadChildProcess.'exit'alone — the subprocess started and then died cleanly or via signal. The'error'listener never fires and is leaked on the deadChildProcess.The orphan handler pins its closure (and the arrow's captured
this) until theChildProcessitself is garbage-collected. Switching naively to.once()would auto-remove the fired handler but leaves the sibling waiting forever for an event that may never come.Fix
Extract a
wireExitListeners(proc, onTerminated)helper that:.once()(the fired handler removes itself).removeListener, so the listener set drops to zero after the first event regardless of which one arrived.onTerminatedso that the legitimate ENOENT case (errorfollowed byexit) still only clears_proconce.The helper accepts a narrow
ProcessExitTargetcontract (just.onceand.removeListener) so the cleanup behaviour is observable against a plainEventEmitterfrom the unit test bed — no real subprocess needed.Test
New unit tests in
src/test/services/sreServer.test.tscover three firing orders:'error'only — callback fires once, both listener counts drop to zero.'exit'only — callback fires once, both listener counts drop to zero.'error'then'exit'(Node's common ordering on spawn failure) — callback fires exactly once, both listener counts drop to zero.Existing tests (
start with invalid python returns not ok, etc.) still cover the live-spawn integration path.Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [LOW, TypeScript].