Skip to content

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.

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.

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:

Terminal window
harn agents lifecycle active
harn 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.

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.

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.

Harnery carries the same two labels through every operator surface:

  • harn agents whoami, list, status, show, and trace, 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.