aws / aws/agentcore-cli

dev: Container agents still orphan the host port on Ctrl+C / process.exit (exit reaper misses the container)

Open
#1,714 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.