Skip to content

fix(process-manager): don't attribute a killed process's exit to its replacement - #1302

Open
pedramamini wants to merge 1 commit into
mainfrom
fix/1044-stale-process-exit-attribution
Open

fix(process-manager): don't attribute a killed process's exit to its replacement#1302
pedramamini wants to merge 1 commit into
mainfrom
fix/1044-stale-process-exit-attribution

Conversation

@pedramamini

@pedramamini pedramamini commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

closes #1044

The report

A session died with exit code 143 (SIGTERM) right after the agent ran a harmless single-file grep whose pattern contained credential-ish keywords. The reporter's hypothesis was a secret-detection guardrail firing SIGTERM. There is no such guardrail in Maestro - the real cause is a process-generation race, which also explains why it fired once and never reproduced.

Root cause

ProcessManager.spawn() kills whatever process currently holds a sessionId before the spawner registers the replacement under that same key:

const existing = this.processes.get(config.sessionId);
if (existing) { /* ... */ this.kill(config.sessionId); }   // SIGTERM, map entry deleted
// ...spawner then does: this.processes.set(sessionId, managedProcess)  // successor

The killed process does not disappear instantly. It drains stdio and fires close (PTY: onExit) a few hundred ms later - after the successor has claimed the key. Every downstream handler is keyed by sessionId alone, so those late events were attributed to the live successor:

  • ExitHandler.handleExit() looked up the successor's managed process and ran detectErrorFromExit(143, ...) against it, emitting agent-error -> "Agent exited with code 143" on a turn that was running fine.
  • It then emit('exit') and processes.delete(sessionId), orphaning the live process - untracked, so the user can no longer stop or interrupt it.
  • Late stdout/stderr from the dead turn was appended to the live turn's buffers.

The renderer's existing "verify the process is actually gone" guard in useAgentExitListener can't catch this: the entry was already deleted by handleExit, so getActiveProcesses() reports it gone and the exit is applied.

The reporter's attached debug package shows the trigger firing twice in the captured window:

1779694272980  Spawning process: ~/.local/bin/claude
1779694272980  [warn] [ProcessManager] Killing existing process before re-spawn
1779694272981  Process spawned successfully
1779694273605  [onExit] Process exit event received      <- predecessor's close, 600ms later

Timing-dependent on a sub-second window, which matches "happened once, never recurred". The credential keywords are a coincidence.

Fix

Each spawner captures its own ManagedProcess and ignores stdio/close/error once a different generation owns the key. A missing entry is deliberately not treated as superseded - that's the ordinary post-kill path, and the renderer needs that exit event to settle the tab.

  • ChildProcessSpawner - guards stdout, stderr, close, error.
  • PtySpawner - guards onData, onExit (and no longer deletes a successor's entry).
  • ExitHandler.handleExit() - re-checks identity before its final emit('exit') + delete, covering the window that opens while it awaits Copilot's on-disk shutdown reconciliation.

This mirrors the identity check OpencodeServerSpawner already performs (if (this.processes.get(sessionId) === managedProcess)).

Scope note: this fixes the mis-attribution, not the re-spawn itself. A spawn landing on a busy key is still logged as a warning, as before.

Tests

7 new regression tests, each verified red before the fix and green after:

  • ChildProcessSpawner.test.ts - late close/error/stdio from a superseded generation emit nothing and leave the successor tracked; the current generation still reports exit normally.
  • PtySpawner.test.ts - same for onExit/onData.
  • ExitHandler.test.ts - a re-spawn during exit handling suppresses the exit event and leaves the successor tracked.

npx vitest run src/__tests__/main - 266 files, 8043 passed. npm run lint and eslint clean.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented output, errors, and exit events from superseded processes from affecting replacement sessions.
    • Ensured replacement processes remain tracked when an earlier process exits during shutdown handling.
    • Preserved correct exit reporting and cleanup for the currently active process.
    • Added safeguards for reused sessions across child-process and terminal-based workflows.

…replacement

ProcessManager.spawn() kills whatever process holds a sessionId before the
spawner registers the replacement under that same key. The killed process
keeps draining stdio and fires `close` (or PTY `onExit`) afterwards, and every
downstream handler is keyed by sessionId alone - so those late events landed on
the live successor:

- its exit code (143 after SIGTERM) was reported through the successor's parser
  as `Agent exited with code 143`, flagging a healthy turn as crashed
- `ExitHandler` deleted the successor's tracking entry, orphaning a running
  process the user could no longer stop or interrupt
- late stdout/stderr from the dead turn was appended to the live turn's buffers

The renderer's existing "is the process still running?" guard couldn't catch
this, because the entry had already been deleted by the time it looked.

Each spawner now captures its own ManagedProcess and drops stdio/exit/error
events once a different generation owns the key (a *missing* entry is not
superseded - that's the ordinary post-kill path whose exit event the renderer
needs to settle the tab). ExitHandler re-checks identity before its final emit
and delete, covering the window that opens while it awaits Copilot's on-disk
shutdown reconciliation. Mirrors the identity check OpencodeServerSpawner
already does.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Superseded process generation handling

Layer / File(s) Summary
Exit handler respawn guard
src/main/process-manager/handlers/ExitHandler.ts, src/__tests__/main/process-manager/handlers/ExitHandler.test.ts
handleExit rechecks the tracked process after shutdown reconciliation and suppresses exit emission for replaced processes; regression coverage verifies the successor remains tracked.
Child-process generation filtering
src/main/process-manager/spawners/ChildProcessSpawner.ts, src/__tests__/main/process-manager/spawners/ChildProcessSpawner.test.ts
Child-process stdout, stderr, close, and error events are ignored when their process instance is no longer tracked, while active-generation exit handling remains covered by tests.
PTY generation filtering
src/main/process-manager/spawners/PtySpawner.ts, src/__tests__/main/process-manager/spawners/PtySpawner.test.ts
PTY data and exit events from replaced processes are ignored; active-generation exit emission and tracking cleanup remain tested.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: copilot, chr1syy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preventing a killed process’s exit from being attributed to its replacement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1044-stale-process-exit-attribution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds process-generation identity checks to prevent replaced child and PTY processes from affecting their successors.

  • Guards child-process stdout, stderr, close, and error callbacks against a different generation owning the session key.
  • Guards PTY data and exit callbacks using the same ownership comparison.
  • Rechecks ownership before ExitHandler emits its final exit event and deletes the tracked process.
  • Adds regression coverage for late events and replacement during exit handling.

Confidence Score: 2/5

This PR should not merge until predecessor side effects are blocked immediately after asynchronous reconciliation and supersession remains detectable after a replacement exits.

The new checks prevent the originally reported ordering while a replacement remains tracked, but exit handling can still mutate replacement state before its final check, and old callbacks become eligible again after the replacement removes the shared map entry.

Files Needing Attention: src/main/process-manager/handlers/ExitHandler.ts, src/main/process-manager/spawners/ChildProcessSpawner.ts, src/main/process-manager/spawners/PtySpawner.ts

Important Files Changed

Filename Overview
src/main/process-manager/handlers/ExitHandler.ts Adds a final ownership check, but places it after predecessor side effects that can still affect a replacement.
src/main/process-manager/spawners/ChildProcessSpawner.ts Guards stale child events while a successor is tracked, but loses generation history after the successor entry is removed.
src/main/process-manager/spawners/PtySpawner.ts Applies the child-process identity pattern to PTY callbacks and retains the same missing-entry race.
src/tests/main/process-manager/handlers/ExitHandler.test.ts Covers final exit suppression but does not assert that earlier post-await output, error, usage, and buffer effects are suppressed.
src/tests/main/process-manager/spawners/ChildProcessSpawner.test.ts Covers stale events while the successor remains tracked, but not events arriving after that successor exits.
src/tests/main/process-manager/spawners/PtySpawner.test.ts Covers stale PTY events while the replacement remains tracked, but not the later missing-entry state.

Sequence Diagram

sequenceDiagram
    participant Old as Predecessor
    participant PM as Process Manager
    participant New as Successor
    participant EH as Exit Handler
    participant UI as Event Consumers
    Old->>EH: close begins exit handling
    EH->>EH: await shutdown reconciliation
    PM->>PM: delete old map entry
    PM->>New: spawn replacement
    PM->>PM: map sessionId to successor
    EH->>UI: post-exit data/error/usage
    EH->>EH: check generation identity
    EH-->>UI: suppress final exit
Loading

Reviews (1): Last reviewed commit: "fix(process-manager): don't attribute a ..." | Re-trigger Greptile

// and the delete would orphan a running process. Callers already screen
// out replaced generations before entering; this re-checks the window
// that opened while we awaited.
if (this.processes.get(sessionId) !== managedProcess) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Late identity check corrupts successor state

When a replacement claims the same session ID during Copilot shutdown reconciliation, post-exit processing resumes before this identity check and writes the predecessor's final output, errors, usage, and completion events through the shared session ID. It can also flush the successor's data buffer, causing the live replacement to receive stale state from the exited process.

Knowledge Base Used: Process Manager

Comment on lines +478 to +481
const isSuperseded = (): boolean => {
const current = this.processes.get(sessionId);
return current !== undefined && current !== managedProcess;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing entry revives stale generation

When the replacement finishes and removes its map entry before the killed predecessor finishes draining, this check starts treating the predecessor as current again. Its late output is forwarded and its SIGTERM close emits a second exit, causing stale output and duplicate renderer or Cue completion effects. The same missing-entry behavior is present in PtySpawner.

Knowledge Base Used: Process Manager

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/process-manager/spawners/ChildProcessSpawner.ts (1)

567-593: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Superseded predecessors now skip temp-image cleanup.

cleanupTempFiles(managedProcess.tempImageFiles) only runs inside handleExit/handleError. Returning early here leaves the predecessor's temp image files on disk for the rest of the app session. Consider cleaning them in the superseded branch.

♻️ Cleanup on the superseded path
 			childProcess.on('close', (code) => {
 				if (isSuperseded()) {
 					logger.warn('[ProcessManager] Ignoring exit from superseded process', 'ProcessManager', {
 						sessionId,
 						pid: childProcess.pid,
 						exitCode: code,
 					});
+					if (managedProcess.tempImageFiles?.length) {
+						cleanupTempFiles(managedProcess.tempImageFiles);
+					}
 					return;
 				}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/process-manager/spawners/ChildProcessSpawner.ts` around lines 567 -
593, Ensure superseded processes still clean up their temporary image files
before returning from the close and error handlers. In the isSuperseded()
branches within the childProcess close/error listeners, invoke cleanupTempFiles
with the managed process’s tempImageFiles, while preserving the existing warning
logs and early-return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/process-manager/handlers/ExitHandler.ts`:
- Around line 258-271: Update the post-await generation check in the
exit-handling flow around managedProcess so it suppresses the exit event only
when sessionId maps to a different existing process; treat a missing
this.processes entry as non-superseded and continue final exit emission.
Preserve the warning and early return for an actual replacement.

---

Nitpick comments:
In `@src/main/process-manager/spawners/ChildProcessSpawner.ts`:
- Around line 567-593: Ensure superseded processes still clean up their
temporary image files before returning from the close and error handlers. In the
isSuperseded() branches within the childProcess close/error listeners, invoke
cleanupTempFiles with the managed process’s tempImageFiles, while preserving the
existing warning logs and early-return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a52bd33-dd8a-499f-af8f-76feed824799

📥 Commits

Reviewing files that changed from the base of the PR and between d2085a0 and 10429e6.

📒 Files selected for processing (6)
  • src/__tests__/main/process-manager/handlers/ExitHandler.test.ts
  • src/__tests__/main/process-manager/spawners/ChildProcessSpawner.test.ts
  • src/__tests__/main/process-manager/spawners/PtySpawner.test.ts
  • src/main/process-manager/handlers/ExitHandler.ts
  • src/main/process-manager/spawners/ChildProcessSpawner.ts
  • src/main/process-manager/spawners/PtySpawner.ts

Comment on lines +258 to +271
// `awaitCopilotShutdown` above can park this handler for seconds, long
// enough for the session to be re-spawned under the same key. Emitting
// then would settle the successor's turn with this process's exit code,
// and the delete would orphan a running process. Callers already screen
// out replaced generations before entering; this re-checks the window
// that opened while we awaited.
if (this.processes.get(sessionId) !== managedProcess) {
logger.warn(
'[ProcessManager] Session re-spawned during exit handling, suppressing exit event',
'ProcessManager',
{ sessionId, code }
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every site that deletes a ProcessManager tracking entry, to see whether any
# delete happens without emitting 'exit' (which the new guard would then swallow).
rg -nP -C4 "processes\.delete\(" --type=ts -g '!**/__tests__/**'

Repository: RunMaestro/Maestro

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files matching ExitHandler.ts =="
fd -a 'ExitHandler\.ts$' . || true

echo "== git diff stat =="
git diff --stat || true

echo "== git diff relevant excerpt =="
git diff -- src/main/process-manager/handlers/ExitHandler.ts | sed -n '1,220p' || true

echo "== outline ExitHandler =="
ast-grep outline src/main/process-manager/handlers/ExitHandler.ts 2>/dev/null || true

echo "== ExitHandler lines 200-320 =="
sed -n '200,320p' src/main/process-manager/handlers/ExitHandler.ts | nl -ba -v200

Repository: RunMaestro/Maestro

Length of output: 743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ExitHandler lines 1-360 =="
sed -n '1,360p' src/main/process-manager/handlers/ExitHandler.ts

echo "== processes.delete occurrences (all ts files) =="
rg -n "processes\.delete\(|\.delete\(" src/main/process-manager -g '*.ts' || true

echo "== process manager handlers =="
find src/main/process-manager -maxdepth 3 -type f -name '*.ts' | sort

Repository: RunMaestro/Maestro

Length of output: 15737


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProcessManager.ts around processes.delete at 300-340 =="
sed -n '280,340p' src/main/process-manager/ProcessManager.ts | nl -ba -v280

echo "== ExitHandler.ts around final delete at 420-475 =="
sed -n '420,475p' src/main/process-manager/handlers/ExitHandler.ts | nl -ba -v420

echo "== PtySpawner.ts around processes.delete at 190-235 =="
sed -n '190,235p' src/main/process-manager/spawners/PtySpawner.ts | nl -ba -v190

echo "== Behavioral probe for the identity guard =="
python3 - <<'PY'
class ManagedProcess:
    name = 'prev'

processes = {'session': ManagedProcess()}
managed_process = processes['session']

def current_process():
    p = processes.get('session')
    return p is managed_process

processes['session'] = None
print('current after removed =', current_process())

processes.clear()
print('current after deleted =', current_process())

processes['session'] = ManagedProcess()
print('current after replaced =', current_process())
PY

Repository: RunMaestro/Maestro

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProcessManager.ts around processes.delete at 300-340 =="
sed -n '280,340p' src/main/process-manager/ProcessManager.ts

echo "== ExitHandler.ts around final delete at 420-475 =="
sed -n '420,475p' src/main/process-manager/handlers/ExitHandler.ts

echo "== PtySpawner.ts around processes.delete at 190-235 =="
sed -n '190,235p' src/main/process-manager/spawners/PtySpawner.ts

echo "== Behavioral probe for the identity guard =="
python3 - <<'PY'
class ManagedProcess:
    name = 'prev'

processes = {'session': ManagedProcess()}
managed_process = processes['session']

def current_process():
    p = processes.get('session')
    return p is managed_process

processes['session'] = None
print('current after removed =', current_process()),

processes.clear()
print('current after deleted =', current_process()),

processes['session'] = ManagedProcess()
print('current after replaced =', current_process())
PY

Repository: RunMaestro/Maestro

Length of output: 4818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProcessManager.ts top/middle and isSuperseded references =="
sed -n '1,120p' src/main/process-manager/ProcessManager.ts
echo "== isSuperseded and set/delete references =="
rg -n "isSuperseded|processes\.set\(|processes\.delete\(|processes\.clear\(" src/main/process-manager/ProcessManager.ts -g '*.ts'
echo "== ProcessManager kill/remove context =="
sed -n '220,350p' src/main/process-manager/ProcessManager.ts

echo "== ChildProcessSpawner onExit semantics =="
sed -n '1,280p' src/main/process-manager/spawners/ChildProcessSpawner.ts

Repository: RunMaestro/Maestro

Length of output: 19941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProcessManager.ts lines 120-250 =="
sed -n '120,250p' src/main/process-manager/ProcessManager.ts

echo "== PtySpawner.ts lines 1-140 =="
sed -n '1,140p' src/main/process-manager/spawners/PtySpawner.ts

Repository: RunMaestro/Maestro

Length of output: 8867


Don’t suppress exit events for removed tracking entries.

If kill() removes the entry while awaitCopilotShutdown is already parked, this.processes.get(sessionId) !== managedProcess also returns true and skips the final exit emission, leaving the tab’s process state unsettled. Treat a missing map entry as non-superseded the same way the PTY spawner does.

🔧 Align with the spawner semantics
-		if (this.processes.get(sessionId) !== managedProcess) {
+		const current = this.processes.get(sessionId);
+		if (current !== undefined && current !== managedProcess) {
 			logger.warn(
 				'[ProcessManager] Session re-spawned during exit handling, suppressing exit event",
 				{ sessionId, code }
 			);
 			return;
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// `awaitCopilotShutdown` above can park this handler for seconds, long
// enough for the session to be re-spawned under the same key. Emitting
// then would settle the successor's turn with this process's exit code,
// and the delete would orphan a running process. Callers already screen
// out replaced generations before entering; this re-checks the window
// that opened while we awaited.
if (this.processes.get(sessionId) !== managedProcess) {
logger.warn(
'[ProcessManager] Session re-spawned during exit handling, suppressing exit event',
'ProcessManager',
{ sessionId, code }
);
return;
}
// `awaitCopilotShutdown` above can park this handler for seconds, long
// enough for the session to be re-spawned under the same key. Emitting
// then would settle the successor's turn with this process's exit code,
// and the delete would orphan a running process. Callers already screen
// out replaced generations before entering; this re-checks the window
// that opened while we awaited.
const current = this.processes.get(sessionId);
if (current !== undefined && current !== managedProcess) {
logger.warn(
'[ProcessManager] Session re-spawned during exit handling, suppressing exit event',
'ProcessManager',
{ sessionId, code }
);
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/process-manager/handlers/ExitHandler.ts` around lines 258 - 271,
Update the post-await generation check in the exit-handling flow around
managedProcess so it suppresses the exit event only when sessionId maps to a
different existing process; treat a missing this.processes entry as
non-superseded and continue final exit emission. Preserve the warning and early
return for an actual replacement.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a process generation race in ProcessManager where late stdout/stderr/exit events from a killed predecessor could be mis-attributed to a newly spawned successor that reuses the same sessionId, leading to incorrect exit reporting and orphaning a live process.

Changes:

  • Add per-spawn identity checks in ChildProcessSpawner and PtySpawner to ignore late events once a different ManagedProcess owns the sessionId.
  • Add a final identity re-check in ExitHandler.handleExit() to avoid emitting exit and deleting the process entry if a re-spawn occurred while exit handling awaited reconciliation work.
  • Add regression tests covering superseded-generation stdout/stderr/exit behavior across both spawner types and within ExitHandler.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main/process-manager/spawners/PtySpawner.ts Ignores PTY onData and onExit events from superseded generations to prevent mis-attribution and accidental deletion of a successor entry.
src/main/process-manager/spawners/ChildProcessSpawner.ts Ignores stdout/stderr/close/error events from superseded generations to avoid attributing predecessor output and exit to a successor.
src/main/process-manager/handlers/ExitHandler.ts Re-checks process identity before emitting exit and deleting the map entry to cover re-spawn during async exit reconciliation.
src/tests/main/process-manager/spawners/PtySpawner.test.ts Adds regression tests verifying late PTY events from a superseded generation are ignored and do not delete the successor entry.
src/tests/main/process-manager/spawners/ChildProcessSpawner.test.ts Adds regression tests verifying late stdio/close/error from a superseded generation are ignored and do not orphan the successor.
src/tests/main/process-manager/handlers/ExitHandler.test.ts Adds a regression test verifying handleExit suppresses the exit event and leaves the successor tracked if re-spawn occurs mid-handler.
Comments suppressed due to low confidence (1)

src/tests/main/process-manager/spawners/ChildProcessSpawner.test.ts:685

  • This newly added comment uses an em-dash (—), which this repo explicitly disallows in any text. Replace it with a plain hyphen or rephrase.
			// handleExit is async (post-exit reconciliation) — let it settle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Regression (issue #1044): ProcessManager.spawn() kills the process holding a
// sessionId before registering the replacement under the same key. The killed
// process drains stdio and fires `close` afterwards, and every downstream
// handler is keyed by sessionId alone — so those late events used to be
Comment on lines +628 to +644
it('ignores a late close from the killed predecessor', async () => {
const { emitter, processes, config, firstHandlers, second } = spawnTwoGenerations();
const onExit = vi.fn();
const onAgentError = vi.fn();
emitter.on('exit', onExit);
emitter.on('agent-error', onAgentError);

// Predecessor finally reports its SIGTERM death (128 + 15).
firstHandlers.get('close')?.(143);
// handleExit is async — give the (suppressed) exit path room to run.
await new Promise((resolve) => setTimeout(resolve, 10));

expect(onExit).not.toHaveBeenCalled();
expect(onAgentError).not.toHaveBeenCalled();
// The live process stays tracked, so the user can still stop it.
expect(processes.get(config.sessionId)).toBe(second);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Session killed with exit code 143 (SIGTERM) during credential-keyword...

2 participants