---
title: Container Session Lifecycle
description: Technical reference for how jackin creates, manages, and tears down container sessions — from supervisor startup through tmux session exit
---
import RepoFile from '../../../components/RepoFile.astro'

## Overview

Every jackin container runs a supervisor as PID 1. Sessions are tmux sessions inside the container — not processes attached directly to the container's stdio. Three session types exist: agent sessions (any session running the agent entrypoint), and shell sessions (ephemeral one-shot zsh access). All agent sessions are equal; there is no "primary" or "secondary" distinction. Closing the operator's terminal detaches from the tmux session, which keeps running inside the container alongside the supervisor.

## Container startup (`jackin load`)

When `jackin load` provisions a new instance, the container launch happens in two distinct steps:

1. `docker run -d --entrypoint /jackin/runtime/supervisor.sh <image>` starts the container detached. The supervisor loop begins immediately as PID 1. No agent runs yet — the container is alive but idle.
2. jackin immediately checks that the supervisor did not exit before the exec step (the `diagnose_premature_exit` check). If the supervisor exited, jackin captures the last 40 lines of container logs and surfaces an actionable error.
3. jackin then runs `docker exec -e TMUX= -it <container> tmux new-session -e JACKIN_AGENT=<agent> -s jackin-<agent>-<id> -- /jackin/runtime/entrypoint.sh [model-flags]`. This call blocks the operator's terminal for the duration of the session.

The session name follows the `jackin-<agent>-<id>` convention where `<id>` is a 6-hex-digit suffix derived from the current nanosecond count (`subsec_nanos() & 0xffffff`). This gives ~16 million distinct values per millisecond. Example: `jackin-claude-1a2b3c`.

`TMUX=` (empty string, not unset) prevents tmux's nested-session warning when the operator's host terminal is itself inside tmux. Setting it to empty rather than unset works because tmux checks `*getenv("TMUX") != '\0'` — an empty value satisfies that check as "no parent tmux session."

Implementation: <RepoFile path="src/runtime/launch.rs" /> (`launch_role_runtime`, `diagnose_premature_exit`).

## Session naming

All agent sessions use the same naming convention: `jackin-<agent>-<id>` where `<agent>` is the agent slug (`claude`, `codex`, `amp`, `kimi`, `opencode`) and `<id>` is a 6-hex-digit nanosecond-derived suffix. There is no concept of a "primary" session — the first session created by `jackin load` and any subsequent sessions created by `jackin hardline` or `hardline --new` all follow the same convention and have equal standing.

## Reconnecting to a session (`jackin hardline`)

`jackin hardline` inspects the target container and chooses the reconnect path:

- **Container is running, sessions exist**: `jackin hardline` runs `docker exec -e TMUX= -it <container> tmux attach-session`. tmux attaches to the most recently used session.
- **Container is running, no sessions**: `jackin hardline` creates a new `jackin-<agent>-<id>` session via `docker exec -e TMUX= -it <container> tmux new-session -e JACKIN_AGENT=<agent> -s jackin-<agent>-<id> -- /jackin/runtime/entrypoint.sh`. The agent slug is read from the instance manifest.
- **Container is stopped (crashed, non-zero exit or OOM)**: Error. Use `jackin load` to start a new session or recover saved state.
- **Container is gone (removed)**: `jackin hardline` can rebuild the runtime around jackin-managed local state (workspace mounts, per-instance durable home, isolation records) when the workspace or ad-hoc directory can be resolved.

Implementation: <RepoFile path="src/runtime/attach.rs" /> (`reconnect_or_create_session`, `hardline_agent`).

## Additional agent sessions (`hardline --new` / console `a`/`A`)

Additional agent sessions allow a second (or third) agent runtime to run inside the same container simultaneously, sharing the same mounted workspace, DinD sidecar, in-container state, and credentials.

Each session gets a unique name: `jackin-<agent>-<id>` using the same convention as the first session. `JACKIN_AGENT=<agent>` is passed via `tmux new-session -e` so the new session can use a different agent runtime than the one originally loaded. The session runs `/jackin/runtime/entrypoint.sh` with `--workdir` pointing at the workspace working directory inside the container.

When an agent session exits cleanly and other sessions remain: the tmux session is removed. The container keeps running. Other sessions are unaffected.

When the last session in the container exits: the tmux server exits, which the supervisor detects within its 1-second poll interval. The supervisor exits 0, which stops the container. The host-side finalize path then runs the full DinD/network/certs teardown (see [Container shutdown](#container-shutdown)).

When an agent crashes (non-zero exit): the tmux session is removed. If other sessions remain, the container keeps running. If it was the last session, the container stops as above and teardown fires.

Both `reconnect_or_create_session` (plain `hardline`) and `spawn_agent_session` (`hardline --new`) set the terminal tab title to the role's display name before the blocking exec, matching the behavior of `jackin load`.

Implementation: <RepoFile path="src/runtime/attach.rs" /> (`spawn_agent_session`).

## Shell sessions (`hardline --shell` / console `x`/`X`)

A shell session opens `docker exec -e TMUX= -it <container> /bin/zsh` directly — no tmux wrapper. It is completely ephemeral: when the shell exits, nothing is left behind. There is no reconnect path.

The container and all other sessions are unaffected by shell session entry and exit. Shell sessions are useful for inspecting the in-container filesystem, running one-off commands, checking installed tooling, or debugging the agent environment without starting another full agent.

Requires the container to be running. If the container is stopped, use `jackin hardline` first (without `--shell`) to restart it, then open a shell.

`spawn_shell_session` also sets the terminal tab title to the role's display name before the blocking exec.

Implementation: <RepoFile path="src/runtime/attach.rs" /> (`spawn_shell_session`).

## Session inventory (`hardline --inspect` / console `i`)

jackin queries `tmux list-sessions -F '#{session_name}'` inside the running container via `docker exec`. The command is run as `sh -c 'tmux list-sessions -F "#{session_name}" 2>/dev/null || true'` so that "no sessions" (tmux exits 1 when there are no sessions) maps to empty output rather than a Docker exec error.

Output: newline-separated tmux session names. Empty output means no sessions are running. If the container is not running, the inventory is reported as `sessions:not_running` without contacting Docker.

Implementation: <RepoFile path="src/runtime/attach.rs" /> (`inspect_agent_sessions`).

## Container shutdown

### Normal exit (agent exits cleanly)

1. **Agent exits** (`/exit` in Claude Code, or the agent process terminates with code 0). The tmux pane closes. If it was the last pane in the session, the session closes. If it was the last session, the tmux server exits and removes its PID namespace.
2. **Supervisor detects exit** within 1 second. <RepoFile path="docker/runtime/supervisor.sh" /> runs `tmux list-sessions` in a poll loop. When the server is gone, `tmux list-sessions` returns non-zero → the `while` loop exits → supervisor exits 0 → container exits 0.
3. **`docker exec` returns** to jackin (the `tmux new-session` command unblocks). The container may still show as `Running` at this instant because the supervisor's exit and Docker's state update are not instantaneous.
4. **`inspect_attach_outcome`** queries `docker inspect` → container is still `Running` → returns `AttachOutcome::still_running()` (exit_code = None).
5. **`finalize_foreground_session`** sees `exit_code=None` and calls `has_tmux_sessions` — `docker exec <container> sh -c 'tmux list-sessions ... 2>/dev/null || true'`. No sessions → proceeds to `finalize_clean_exit` (isolation worktrees swept, clones removed).
6. **`inspect_container_state`** queries Docker again. By now the container has usually stopped (Stopped/0) or is still Running (supervisor lag):
   - **Stopped/0**: `run_clean_exit_teardown` runs directly.
   - **Running**: `!is_preserved` (finalize returned Cleaned) → `run_clean_exit_teardown` runs without re-querying sessions.
7. **`cleanup.run()`** removes all resources in order:
   - `docker rm -f <role-container>`
   - `docker rm -f <dind-container>`
   - `docker volume rm <certs-volume>`
   - `docker network rm <network>`
8. **Caffeinate reconcile** (`caffeinate::reconcile`): no containers carry `jackin.keep_awake=true` anymore → `want_running=false`, `liveness=Alive` → `stop_caffeinate(pid)` → `kill <caffeinate-pid>`. In `--debug` output this appears as `[jackin debug keep_awake] stopping caffeinate (PID N)` followed by `[jackin debug cmd] kill N`.

### Forced stop (`docker stop`)

`docker stop <container>` sends SIGTERM to PID 1 (the supervisor). The trap in <RepoFile path="docker/runtime/supervisor.sh" /> kills the background sleep and calls `exit 0`. The container exits cleanly, then host-side cleanup fires through the same path as a normal exit.

One important caveat: SIGTERM is **not forwarded** to processes started via `docker exec` (the tmux server and agent processes inside it). They are killed when the PID namespace tears down after the supervisor exits. Agents that flush state on SIGTERM do not receive that signal when `docker stop` runs — they are killed directly by the kernel as the namespace closes.

### Detach (Ctrl-B D)

When the operator detaches from tmux (`Ctrl-B D`) instead of exiting the agent, the `docker exec` process returns but the tmux session and server remain alive inside the container. The supervisor keeps running (sessions still present). On the host side:

- `inspect_attach_outcome` → container is `Running` → `still_running()`.
- `finalize_foreground_session` calls `has_tmux_sessions` → sessions found → returns `Preserved`.
- Container state match: `is_preserved=true` → re-checks sessions via `inspect_agent_sessions` → sessions still present → `cleanup.disarm()`.
- The DinD, network, and certs volume all remain. `jackin hardline` can reattach to the running session.

### Crash (non-zero exit or OOM)

When the container exits with a non-zero code or OOM, `inspect_container_state` returns `Stopped { exit_code: nonzero, ... }` or `Stopped { oom_killed: true, ... }`. jackin writes `InstanceStatus::Crashed` and runs `cleanup.run()` — DinD, network, and certs volume are all removed. The instance manifest records the crash so `jackin console` surfaces it as a recoverable state.

## What persists across session exit

| Thing | Persists? | Notes |
|---|---|---|
| Agent conversation history | Yes | Stored in the per-instance durable home mount on the host |
| Agent auth tokens | Yes (per auth mode) | Depends on the configured auth forwarding mode |
| Files written in the mounted workspace | Yes | Host-side mount |
| In-container packages installed ad hoc | No | Writable layer lost on container stop/removal |
| DinD images | No | DinD volume is recreated on rebuild |
| Tmux session state | No | Removed when session exits or container stops |
| Agent tmux sessions | No | Removed when agent exits; recreated by `hardline` or next `load` |

## Interaction diagram

```
jackin load
  └─ docker run -d (supervisor.sh as PID 1 — polls tmux list-sessions every 1s)
  └─ docker exec tmux new-session -s jackin-claude-1a2b3c → [SESSION]
       │
       ├─ operator detaches (Ctrl-B D) → session alive in tmux, container running
       │     └─ jackin hardline → docker exec tmux attach-session → reattach
       │     └─ DinD/network preserved while container running with sessions
       │
       ├─ jackin hardline --new → docker exec tmux new-session -s jackin-codex-abc123
       │     └─ [SECOND SESSION] — independent lifecycle, equal standing
       │     └─ both sessions must exit before supervisor stops the container
       │
       ├─ jackin hardline --shell → docker exec /bin/zsh → [SHELL SESSION, no tmux]
       │
       └─ agent exits (/exit or process ends) → tmux session removed
             ├─ other sessions remain → container keeps running
             └─ last session exits → tmux server exits → supervisor detects (≤1s)
                   └─ supervisor exits 0 → container exits 0
                   └─ jackin finalize:
                       ├─ has_tmux_sessions check → no sessions → finalize_clean_exit
                       ├─ isolation worktrees swept (git status, clones removed)
                       └─ cleanup.run():
                           ├─ docker rm -f <role-container>
                           ├─ docker rm -f <dind-container>
                           ├─ docker volume rm <certs-volume>
                           ├─ docker network rm <network>
                           └─ caffeinate reconcile → kill <caffeinate-pid>
```
