dev: Container agents still orphan the host port on Ctrl+C / process.exit (exit reaper misses the container)
- Dominant language
- TypeScript
- Stars
- 283
- Forks
- 95
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 183
Description
## Summary
The exit-reaper added in #1695 (`registerExitCleanup` in `src/cli/operations/dev/dev-server.ts`) fixes the orphaned-child-holding-the-port bug (#1690) for **CodeZip** dev servers, but **not for Container agents**. On the `process.exit(0)` path — i.e. `Ctrl+C` / `kill $PID`, which is the exact scenario #1695 targets — a Container dev server keeps running and continues to hold the mapped host port.
## Root cause
The reaper reaps the child's **process group**:
```ts
// dev-server.ts
process.kill(-pid, 'SIGKILL');
```
For `ContainerDevServer`, `getSpawnConfig()` returns `docker/podman/finch run --rm --name -p : ...`, so `this.child` is the **run client**, not the server. The actual server runs inside the container, owned by `dockerd`/`containerd` in a separate process tree. Group-killing the run-client's process group does **not** stop the container:
- `--rm` cleans up on container *stop/exit*, not on client death — so it never triggers here.
- SIGKILL to the run client does not propagate to the container for the client/daemon split (true for Docker, Podman/conmon, and Finch/Lima).
Container teardown lives **only** in the `ContainerDevServer.kill()` override (`spawn(runtimeBinary, ['stop', containerName])`), and the `process.exit(0)` path bypasses `kill()` entirely — that bypass is the whole reason the reaper exists.
## Repro
```bash
# On a Container agent (agent with a Dockerfile / --type container)
agentcore dev --logs --port 9137 &
DEV_PID=$!
# wait for the port to bind, then:
kill $DEV_PID # or Ctrl+C in an interactive session
sleep 4
lsof -ti :9137
# actual: container still running, port held (reparented; only freed by manual `docker stop`)
# expected: empty — port freed
```
The next `agentcore dev` then fails on the held port until the user manually runs `docker stop `. (Note: the base `prepare()` `rm -f` clears a stale *name* on next launch, but not a still-running container's live port binding.)
## Suggested fix
Make the exit reaper container-aware — on the exit path, stop the container by name (mirroring the `kill()` override's `runtimeBinary stop `) rather than group-killing the run-client pid. Since `process.exit`'s `'exit'` handler must be synchronous, a `spawnSync(runtimeBinary, ['stop', containerName])` (or `rm -f`) is likely the right primitive there.
## Context
- Regression status: **not a regression from #1695** — container mode was already uncleaned on the `process.exit` path before that PR. #1695 is a strict improvement (fixes CodeZip); this issue tracks extending the same protection to containers.
- The e2e verification in #1695 used a **TypeScript (CodeZip)** agent, which is exactly the case the group-kill handles, so the container path was not exercised.
## Related
- #1695 (the CodeZip fix)
- #1690 / #1440 (original orphaned-port bug)
- #1438 (detached spawn + process-group tree-kill)
Contributor guide
Research direction
Start in src/cli/operations/dev/dev-server.ts at registerExitCleanup and compare its process.exit path with ContainerDevServer.kill(), which stops the named container through the runtime binary. Reproduce with the provided agentcore dev and lsof commands; done means Ctrl+C or process.exit stops the container and leaves the host port free for the next launch.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, typescript
- Domain
- cli, devops
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100