The layers
Most explicit first. A harness that reports itself is believed over anything inferred.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.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-kinds, 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.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 howaspect worktree add normally arrives:
- Gemini CLI sets
GEMINI_CLI=1for shell tools and givesGEMINI_SESSION_IDto 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.
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.
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:- 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.
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.- 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.
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 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:
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.
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.
