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.started, session.resumed, agent.started idle The session exists, but no open turn is implied.
turn.started, tool.requested working A turn or tool action is in progress.
command.started working Applied only during an already evidenced open turn.
wait.started needs_input The adapter directly reported an operator prompt.
wait.ended working The reported wait ended and execution resumed.
turn.completed, session.ended, agent.completed 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 wait.started. 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 coord.lifecycle_changed 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.

Task lifecycle and session lifecycle are separate. lifecycle done says the declared task is complete; agents end records terminal authority for the V3 session. A completed response (turn.completed) ends only one response.

Native harness callbacks remain the strongest end signal. When a harness lacks one, Harnery can approve an explicit end, wait through a reversible archive grace period, or reconcile idle, parent/run completion, stale, superseded, and host-loss observations. Every path converges on one leased finalizer, so a generation receives at most one terminal event and cannot be resurrected.

An agent can request its own end from the closing status command even though that command still belongs to an open tool span and turn. Harnery persists the intent, waits for the matching tool.completed and turn.completed, then lets the stop hook record session.ended. Starting new work cancels the request. The terminal row therefore never appears before the work that proves the turn closed.

Automatic policy ends clean coordination state but do not qualify as run-quality evidence. Verified archive ends require an independent ambiguity review before corpus admission.

A lifecycle change is title-silent unless it marks the session done:

Lifecycle Suggested name
active No title change
blocked No title change
done [DONE] Agent <name> - <task>

The coord.lifecycle_changed event carries suggested_session_name only for the done projection. Prompt context keeps reminding the agent until that name is observed, and the display latch treats it like the first session name. Reopening does not print another title. 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": "wait.started",
"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.