Session state
Harnery describes a session with two independent state axes.
For a concise explanation you can share, start with the Session state feature page. To apply the same model to Claude Desktop, use the session-management guide.
| Axis | Values | Question answered |
|---|---|---|
activity |
unknown, working, needs_input, idle |
What is the agent doing now? |
task_state |
active, blocked, done |
Is the declared task still open? |
The separation matters. An agent waiting for approval can have
activity: needs_input and task_state: active. After the turn ends, it can
be idle while the task remains active. Neither state implies the other.
Activity evidence
Section titled “Activity evidence”Harnery changes activity only when a canonical event supplies evidence.
| Canonical event | Activity | Notes |
|---|---|---|
session.start, subagent.start |
idle |
The session exists, but no open turn is implied. |
user_prompt.submit, tool.pre_use |
working |
A turn or tool action is in progress. |
command.start |
working |
Applied only during an already evidenced open turn. |
interaction.input_requested |
needs_input |
The adapter directly reported an operator prompt. |
turn.stop, session.end, subagent.stop |
idle |
The current execution window ended. |
Output chunks, post-tool events, narration, unrelated coordination commands,
and elapsed time do not clear needs_input. New progress evidence or a stop
event does.
Codex provides a PermissionRequest hook, which Harnery records as
interaction.input_requested. Claude Code and Cursor sessions still use
unknown when their available events cannot prove an input wait. Harnery does
not turn a nearby pre-execution event into approval evidence.
Lifecycle evidence
Section titled “Lifecycle evidence”Only state.task_state changes task lifecycle. Stopping a process or reaching
the end of a turn does not mark the task done.
Human-facing sessions declare lifecycle explicitly:
harn agents lifecycle activeharn agents lifecycle blocked --reason "waiting for repository access"harn agents lifecycle done --reason "implementation and verification complete"blocked requires a reason. Leaving blocked clears that reason. Repeating
the same state and reason records nothing, which keeps replay idempotent.
Subagents, transient processes, and workflow children cannot make these
operator-facing declarations.
done also requires a current task and a passing Git finalization check. The
check covers the session’s durable write-claim history, dirty touched paths,
enclosing submodule pointers, repository verification, and remote reachability.
A refusal writes no lifecycle event and leaves the heartbeat unchanged. This
gate certifies repository hygiene, not tests or business correctness.
The per-turn agents set-task command remains lifecycle-neutral. Changing task
text while blocked or done returns a warning, but it does not reopen the task.
Use lifecycle active to resume explicitly.
The compatibility default is active when an older heartbeat or event stream
has no lifecycle event. A blocked state may carry a reason. Reopening the task
clears an obsolete blocker reason.
Session-name projection
Section titled “Session-name projection”A lifecycle change can re-mint the human-facing session name:
| Lifecycle | Suggested name |
|---|---|
active |
Agent <name> - <task> |
blocked |
[BLOCKED] - Agent <name> - <task> |
done |
[DONE] - Agent <name> - <task> |
The state.task_state event carries suggested_session_name and
name_reminted: true when the name changes. Prompt context keeps reminding the
agent until the new name is observed, and the Claude Code Stop rule treats the
re-mint like the first session name. The title is still a projection. Editing
it does not mutate lifecycle.
Claude Desktop cleanup uses the same one-way projection. harn claude-desktop tidy maps a sidebar entry to its Harnery session and checks the latest durable
lifecycle event. It archives mapped done entries only when --yes is present.
A [DONE] title without lifecycle evidence is skipped unless the operator adds
--include-legacy-prefix; a [BLOCKED] title never qualifies. Titles remain an
output of lifecycle, not an input to it.
Live state and replay
Section titled “Live state and replay”The heartbeat projector writes these fields to .harnery/active/<id>.json:
{ "activity": "needs_input", "activity_updated_at": "2026-08-12T10:02:00.000Z", "activity_source": "interaction.input_requested", "task_state": "active", "task_state_updated_at": "2026-08-12T09:30:00.000Z"}Heartbeats are a live projection. The canonical event ledger is the durable
record. The public foldSessionState() helper rebuilds both axes from a session
slice, even after its heartbeat has been removed. Live projection and replay
call the same transition reducer. Replay orders evidence by event timestamp,
so an older event appended later cannot roll the state backward.
Where the state appears
Section titled “Where the state appears”Harnery carries the same two labels through every operator surface:
harn agents whoami,list,status,show, andtrace, including JSON- prompt-time peer context and its change-detection hash
- cross-machine presence blobs and remote peer readers
- web cards, agent chips, the agent detail page, and ended-session detail reads
The presence hash includes activity, lifecycle, and the bounded blocker reason. A state-only change therefore reaches another machine without waiting for a task or file claim to change. The web refresh signal also observes heartbeat writes, so lifecycle-only updates repaint the live view.
Ended and swept heartbeats disappear from the live board. Their event-derived identity and session state remain available to history and detail readers. This keeps the live board honest without discarding the lifecycle record.
See ADR 0072 for the decision and rejected inference rules.