Skip to content

Recover after a context compaction

MCP onlyIntermediate~5 min

When you work as a named team member — Iris, Dara, Tove — on an MCP-only host like Claude Desktop or Claude for Mac, your working identity lives entirely in the conversation. hadron_start_session opens a worker session and hands you two things exactly once:

  • the worker-session id, which is what makes your work attributable, and
  • the worker's boot briefing, which is who you are for the rest of the session.

Nothing on the wire carries either one afterwards. There is no file to read them back from, the way the CLI has a worktree binding. So when the host compacts the chat session — the conversation you are in — both can simply be gone, and the agent carries on wearing the name without being able to act as the worker.

The worker session itself is untouched by any of this. It is still open on the server, still holding the worker; only your side of it was lost. That is the whole reason recovery works — and the reason a forgotten worker session keeps a worker taken long after its chat session is closed. See the two kinds of session.

This page is the way back. It takes two read-only calls and costs nothing.

The failure that doesn't announce itself

Losing the worker-session id is worth catching early, because its worst consequence is silent.

A missing worker-session id doesn't error — it changes the author

hadron_team_chat_post takes session as an optional argument. Post without it and the call succeeds: the message is simply recorded as authored by you, the human, instead of by the worker.

No error, no warning, and the team chat looks fine — until someone reads the history and finds the teammate's work signed by somebody else. This is precisely the anonymity the Worker model exists to remove.

The other two are at least loud: hadron_record_work requires session and refuses without it, and hadron_end_session needs the id to end anything.

So: if you are unsure whether you still have your worker-session id, assume you don't, and check. The check is one call.

Step 1 — find your worker session

hadron_whoami()

It answers "what am I driving right now?" with your own open worker sessions:

1 session(s):
- session 019f2a30-5014-734c-a1ab-0000deadbeef
    working as: Iris (backend-engineer)
    hrn:worker:acme.com:eng-team:iris
    app: Eng Team
    started: 2026-08-17T08:12:04.000Z · DEVELOPER
    plan: Wire the roster read

Pass the session id to hadron_team_chat_post (to post AS the worker rather than
as yourself), hadron_record_work, and hadron_end_session. Re-fetch a worker's
boot briefing with hadron_get_worker.

That id is the thing you thread back into the team tools.

A few things worth knowing about the answer:

  • It is scoped to worker sessions you started — it is not a presence board for the team. To see who else is driving what, use hadron_list_workers, which reports each worker as taken or free.
  • It needs no App selected, which matters here: the active-App selection is per-connection state and may have evaporated for the same reason your context did.
  • Pass includeEnded: true to also list recently ended worker sessions — useful to confirm you really did end one, or to find the worker session a work record should have been attributed to.

If it reports no open sessions, no worker session is bound and you are working as yourself. Start one — note that description is required alongside workerRef:

hadron_start_session(description: "Resuming the roster work", workerRef: "Iris")

A session bound to no worker

hadron_whoami calls this out explicitly:

working as: nobody — this session is NOT bound to a worker, so anything it
posts is attributed to you, not a teammate.

A Session can exist with no worker bound to it — and then it is not a worker session at all, just a session. Passing its id to hadron_team_chat_post will not make you post as a teammate, because there is no teammate on it. If you meant to work as a worker, end this session and start one with workerRef.

Step 2 — re-fetch your briefing

Pass the session you just recovered. A compaction may also have cost you the active-App selection, and a bare name is only unique within an App — the session names its own App, so this form works either way:

hadron_get_worker(workerRef: "Iris", session: "019f2a30-5014-734c-a1ab-0000deadbeef")

(hadron_get_worker(workerRef: "Iris") is enough when an App is still selected, and a URN or id needs neither.)

**Iris** (backend-engineer) — taken
Worker id: 019f1c02-88be-7a0e-9d31-0000c0ffee00
Worker URN: hrn:worker:acme.com:eng-team:iris
Role agent: Backend Engineer
Your active session: 019f2a30-5014-734c-a1ab-0000deadbeef (since 2026-08-17T08:12:04.000Z)

Boot briefing — adopt this for the rest of the session:

You are Iris, the backend-engineer on this team. …

(The tool's reply is Markdown, so your host renders **Iris** as Iris — the asterisks above are the literal bytes on the wire.)

Adopt the briefing again, exactly as you did at bind time.

It is rendered live, from the role agent's prompt template plus this worker's own promptOverride — which is why the contract tells you never to cache or copy it. A copy goes stale silently the moment someone edits the role's template or amends the worker's override; this read cannot.

workerRef takes the same three forms as hadron_start_session:

Form Example Needs App context?
Name Iris Yes — a name is unique only within an App
URN hrn:worker:acme.com:eng-team:iris No
Id 019f1c02-… No

App context can come from an already-selected App, an explicit app, or — as above — the session. If you have none of the three, use the worker's URN or id.

A worker you may not read reports not found

Not "forbidden". The answer for a worker in an App you don't belong to is identical to the answer for a name that App has never used, so a wrong name and a wrong App look the same. Check the spelling and the App before concluding the worker is gone.

Don't recover these other ways

Two routes look like they would work and shouldn't be used:

  • Starting a second worker session for the same worker. You now have two open worker sessions for one worker, and the provenance record — which PR came from which session, driven by whom — stops meaning anything.
  • hadron_start_session(..., force: true). force exists for an informed takeover: a human deciding to take a worker off someone else. Using it on yourself churns the session record and rewrites "last driven by" for no reason. It is also, by contract, only to be used after surfacing the current driver to the user and getting an explicit override.

Both are writes. The two calls above are reads — they reserve nothing, mint nothing, and leave no trace.

When you're done, end the worker session

The mirror image of this page's problem, and the one that inconveniences other people rather than you:

hadron_end_session(id: "<id>", summary: "Wired the roster read")

Closing the chat session does not do this. Archive the conversation without ending the worker session and the worker stays taken — so the next teammate to pick up Iris gets a WORKER_TAKEN prompt naming you, for work you finished yesterday. The stale-session reaper clears it eventually, but "eventually" is up to a day.

Ending the session clears taken. It does not release the worker's name, which stays held by you until an explicit release — and on this surface there is currently no way to release it (hadron-server#1060). So if a teammate needs Iris rather than a worker of their own, that hand-back has to happen through the API or an admin today.

Confirm you're actually back

Post something small and check the author:

hadron_team_chat_post(body: "Back after a compaction — resuming.", session: "<id>")

The response names the author it recorded:

Posted team-chat message #57 as Iris.

If that says your own name instead of the worker's, the session argument didn't reach the call — which is the whole failure this page exists to prevent.

On the CLI this doesn't arise

The hadron CLI writes its session binding into the worktree, so the worker-session id survives anything that happens to a chat session, and hadron team session whoami reads it back. The gap is specific to MCP-only hosts, which have no equivalent place to keep it.