> ## Documentation Index
> Fetch the complete documentation index at: https://aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Which agent holds a slot

> How the pool works out which coding agent holds each slot, what each harness reports, and why some sessions can be resumed and reclaimed while others can only be named.

<Warning>
  New and experimental. File bugs and suggestions to [aspect-build/aspect-cli/issues](https://github.com/aspect-build/aspect-cli/issues).
</Warning>

A lease records the harness and the session that took it, so a machine running several agents lists as something you can act on rather than a column of hashes.

```shell theme={null}
aspect worktree list
```

```
~/.aspect/worktrees/github.com/acme/web/aspect-worktree-<slot>
2 slots: 2 taken, 0 free

taken
  slot          state   warmth  age  leases  branch      agent        session
  18a11aa20e8d  in use  warm    12d       7  fix/login   claude-code  b22fe20c-1111-4000-8000-00000000b22f
  b2697ff9b4e3  in use  cold    12d       4  spike/perf  codex        019edfd4-5a2b-7c3d-8e4f-0123456789ab

  For more details run: aspect worktree list --verbose
  For usage instructions run: aspect worktree list --help
```

None of this decides anything on its own. It is description: **nothing takes a slot away on the strength of a detected identity.**

## The layers

Most explicit first. A harness that reports itself is believed over anything inferred.

| | How | Authoritative |
| - | - | - |
| 1 | `--agent-kind` / `--agent-id` on the command | yes |
| 2 | `ASPECT_AGENT_KIND` / `ASPECT_AGENT_ID` | yes |
| 3 | A known harness's own variables | reported by the harness |
| 4 | The process tree — walk up until a recognised binary appears | inferred |
| 5 | `AI_AGENT`, which names a tool and not a session | inferred |

<Note>
  **A subagent is recorded as its parent session.** Claude Code runs subagents inside the parent's process and passes them the parent's environment, so a subagent's tool calls carry the same `CLAUDE_CODE_SESSION_ID` and the same process tree — nothing a command can read tells the two apart. A lease a subagent takes is listed under the parent's session, and its resume command returns to the parent conversation, which is the one you can resume.
</Note>

`agent.source` in `--output=json` says where the identity came from: `flag` for the flags; `env` for any environment variable, layers 2, 3 and 5; `ancestry` for the process tree.

Layers 1 and 2 **override the field they name and leave the rest to detection**, field by field — a wrapper's `ASPECT_AGENT_KIND` and a per-call `--agent-id` combine. Passing `--agent-id` to tell sibling agents of one session apart keeps the kind and the process that detection found, which is what liveness depends on, and records the harness's own session as `parent_id`: that is the session its store holds and its resume command returns to, so the lease is named "claude-code agent worker-a in session …". Where detection finds no session id of its own — a wrapper exporting its harness's real session through `ASPECT_AGENT_ID`, with its kind in `ASPECT_AGENT_KIND` — the id given *is* the session, and is resumed as such. Without a kind there is no harness to resume it in. Naming a *different* harness, with `--agent-kind` or `ASPECT_AGENT_KIND`, drops the detected process, since a pid identifies the harness it was detected for and answering liveness about the wrong process is how a slot gets taken from somebody.

An id is printed in tables, messages and suggested commands, so it has to be visible text. One given with `--agent-id` or `ASPECT_AGENT_ID` that is all spaces, longer than 128 characters, or has a character a terminal would act on or hide — a C0 or C1 control character, DEL, the soft hyphen, the combining grapheme joiner, a Hangul or Khmer filler, an Arabic or Mongolian format mark, a zero-width character or joiner, a line or paragraph separator, a bidirectional override or isolate, an invisible operator, a variation selector, a musical formatting character, an interlinear annotation mark, a byte-order mark or a tag character — is refused with `invalid_agent`, by every command. The message shows the value in double quotes, each such character written once as a `\u` escape such as `\u202e`, or as `\U` and eight hex digits above U+FFFF. A session id a harness reports with such a character is not taken as an id: the lease records the kind and process detection found, without one.

### Whether a lease is yours

`release`, `path` and the rest decide whether a lease is the caller's own by comparing the two records. A session id the harness itself reported — `CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID` and the like, kept as `harness_id` through any relabelling — matches across `--agent-kind`s, so naming a session's kind differently does not make its own lease somebody else's. An id the caller chose, with `--agent-id` or `ASPECT_AGENT_ID`, matches only within its kind. Two records that each name a harness session match only when it is the same one: records naming different harness sessions never match, even when their subagents chose the same `--agent-id`. With no id on either side, two records of one kind match by harness process, pid and start time, where one was captured, and by kind alone where neither was.

## What each harness tells us

A harness can mark its presence, name the session, or both — and the difference decides how much the pool can do for you.

| Harness | Recognised by | Names the session? | Resume |
| - | - | - | - |
| Claude Code | `CLAUDECODE` | yes, `CLAUDE_CODE_SESSION_ID` | `claude --resume <id>` |
| Codex | `CODEX_THREAD_ID` | yes | `codex resume <id>` |
| Grok Build | `GROK_SESSION_ID` | yes, from 1.0.4 | `grok --resume <id>` |
| Gemini CLI | `GEMINI_CLI` | **no** — except to hooks | `gemini --resume <id>` |
| Cursor | `CURSOR_AGENT` | **no** | `cursor-agent --resume <id>` |
| aider, goose, opencode, amp | the process tree | no | — |

### Gemini CLI and Cursor are detected, but their sessions are not

Both export a variable saying they are running, and neither puts a session id into the environment a *tool command* runs in — which is how `aspect worktree add` normally arrives:

* **Gemini CLI** sets `GEMINI_CLI=1` for shell tools and gives `GEMINI_SESSION_ID` to **hook** processes only.
* **Cursor** sets `CURSOR_AGENT`, whose value is deliberately unspecified — its own docs say to test that it is *set* — and passes the conversation id on a hook's **stdin**, where no environment variable carries it.

So a slot held by either lists as `gemini-cli` or `cursor` with nothing beside it, which is the honest answer: the pool knows which tool is working, not which conversation. Three consequences:

* **No resume command is offered**, because there is nothing to resume *to*. The spelling is known for both; the id is not.
* **The slot is not reclaimed on the strength of the session**, because there is no session to look up. Its process is found by walking the process tree, as for any harness, and the slot is reclaimed once that process is gone, the grace period has passed and the checkout is clean — see below.
* **Two of its sessions are told apart by that process.** Where none was found, nothing distinguishes them, so one can release the other's lease; a dirty checkout is still refused.

You can supply what the harness does not. A wrapper that exports `ASPECT_AGENT_KIND=cursor` and `ASPECT_AGENT_ID=<chat id>` gets the session named and a resume command in `--verbose`, and keeps the process detection found, for any harness in the table.

## What makes a session resumable, and what makes a slot reclaimable

These are different questions, and the pool keeps them apart.

**Resumable** means the harness's own session store holds the session. Claude Code files one under a directory named for the path it started in; Grok Build does the same. Codex files by date, and Cursor under a hash of the workspace, so neither can be looked up this way — and a resume command is offered for them anyway, because nothing says it would fail. What is never offered is a command for a session the store *was* read for and did not have.

**Reclaimable** means somebody else may take the slot, and it needs three things that have nothing to do with the store:

1. The holder's recorded process is not running. A live holder keeps its slot however long since its last build — idleness measures Bazel, not whether somebody is reading code.
2. `Worktrees.abandoned_grace_hours` (24 by default) have passed since the slot's last activity — the lease being taken, Bazel using its output base, or a git operation in the checkout — so a session closed mid-task and resumed the next morning finds its slot. 0 drops this condition.
3. The checkout is clean. Commits survive removing a worktree; uncommitted changes do not, so a dirty slot is never taken however certainly its holder is gone.

**A resumed session keeps its slots.** A resumed session — `claude --resume`, say — runs in a new process, which its leases do not record. When it runs `path` for a slot it may enter, `inspect` anywhere in the clone or its slots, or `add` of a branch that is already leased, every lease of that harness session — its own and its subagents' — whose recorded process is no longer running moves to the caller's live process. So the resumed session and everything it ran read as running, and their slots are not taken back once the grace period is up. A lease whose recorded process is still running stays where it is; a lease with no session id is not moved, and nor is one whose holder's process was never captured.

Leases are matched by the session id the harness reported: the same `harness_id`, or, for a record without one, the harness session as the holder's id or its `parent_id`. An id a caller chose with `--agent-id` or `ASPECT_AGENT_ID` matches nothing here, since two sessions can choose the same one, so a session whose harness reports no session id — Gemini CLI, Cursor — does not have its leases moved on resume.

The question is only asked when [`add`](/docs/cli/worktrees/add) needs a slot and every one is in use, or when the lease's own branch is asked for again — so the alternative is another checkout and another output base on disk, and a returning session loses nothing it cannot recover: its branch and commits are untouched, and asking for that branch again is likely to land on this same slot, still warm.

A lease with **no agent recorded** — a worktree you took yourself — is never reclaimed, and nor is one whose holder's process was never captured. The rule is that the process is *verifiably* gone, and verifying that needs to know which process it was.

## Session states

`session_state` in `--output=json`, and the `notes` column where it is worth acting on:

| | |
| - | - |
| `running` | The recorded process is alive, with a matching start time |
| `resumable` | That process is gone, but the session is still stored |
| `gone` | The store was read and does not have the session |
| `unknown` | No way to tell, for this harness or this record |

`unknown` is the common answer. `unknown` gets no session note and no resume hint under the table — though a lease whose recorded process has stopped still reads `process stopped` — but reclaiming never looks at the session state: only the recorded process, the grace period and a clean checkout decide.

<Note>
  This is a claim about the **process** recorded with one lease, not about the session. One session can take slots from several processes, so the same session id may read `running` on one row and `resumable` on another — each row reporting the process that took it.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.