Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(typescript/agent-os-vscode): release sreServer exit listeners on whichever event fires first#2176

Merged
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/typescript-sreserver-listener-cleanup
May 12, 2026
Merged

fix(typescript/agent-os-vscode): release sreServer exit listeners on whichever event fires first#2176
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/typescript-sreserver-listener-cleanup

Conversation

@finnoybu

Copy link
Copy Markdown
Contributor

Problem

SREServerManager.start registered plain (long-lived) listeners on the spawned python -m agent_failsafe.rest_server subprocess:

this._proc.on('error', () => { this._proc = undefined; });
this._proc.on('exit',  () => { this._proc = undefined; });

Node's child_process can 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 = undefined is set twice; that's harmless on its own, but the handlers stay registered on the dead ChildProcess.
  • 'exit' alone — the subprocess started and then died cleanly or via signal. The 'error' listener never fires and is leaked on the dead ChildProcess.

The orphan handler pins its closure (and the arrow's captured this) until the ChildProcess itself 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:

  • Registers both with .once() (the fired handler removes itself).
  • AND, on first fire, the handler proactively removes the sibling via removeListener, so the listener set drops to zero after the first event regardless of which one arrived.
  • Debounces onTerminated so that the legitimate ENOENT case (error followed by exit) still only clears _proc once.

The helper accepts a narrow ProcessExitTarget contract (just .once and .removeListener) so the cleanup behaviour is observable against a plain EventEmitter from the unit test bed — no real subprocess needed.

Test

New unit tests in src/test/services/sreServer.test.ts cover three firing orders:

  1. 'error' only — callback fires once, both listener counts drop to zero.
  2. 'exit' only — callback fires once, both listener counts drop to zero.
  3. '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].

…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.
@github-actions

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

No security issues found.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `sreServer.ts`

sreServer.ts

  • wireExitListeners -- validate that both listeners are removed after the first event fires.
  • wireExitListeners -- ensure callback fires once for an 'exit' event alone.
  • wireExitListeners -- check callback fires once for an 'error' followed by an 'exit' event.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

TL;DR: 0 blockers, 0 warnings. No issues found. Clean change.

@github-actions github-actions Bot added the size/M Medium PR (< 200 lines) label May 12, 2026
@github-actions

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

Docs Sync

  • wireExitListeners() in sreServer.ts -- missing docstring
  • README.md -- section on SREServerManager needs update
  • CHANGELOG.md -- missing entry for behavioral changes regarding exit listeners in SREServerManager

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

API Compatibility

Severity Change Impact
Low Replaced long-lived listeners on the subprocess with mutually removing one-shot listeners. Potentially changes the behavior of event handling for subprocess errors and exits, which could affect any dependent functionality relying on the previous listener behavior.

@github-actions

Copy link
Copy Markdown

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential NONE
Overall MEDIUM

Automated check by AGT Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label May 12, 2026
@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ✅ Passed No issues found
🛡️ Security Scan ✅ Passed No issues found
🔄 Breaking Changes ✅ Completed Analysis complete
📝 Docs Sync ✅ Completed Analysis complete
🧪 Test Coverage ✅ Completed Analysis complete

Verdict: ✅ Ready for human review

@imran-siddique imran-siddique merged commit d7a9900 into microsoft:main May 12, 2026
13 of 14 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk size/M Medium PR (< 200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants