[BUG] archive_session refuses a pinned session, and the refusal can't say which of four conditions blocked it

Status Open
Reported on v2.1.236
Maintainer reply None cached
Activity 0 comments · opened Sep 10, 2026

What's wrong

mcp__ccd_session_mgmt__archive_session refuses to archive a session that is pinned in the Code-tab sidebar, and the refusal message cannot tell the caller that the pin was the reason.

A Code session finished its work and called archive_session on itself. The call was refused:

Session self ("<title>") was not archived: the app is keeping it for the user (pinned or in use). Wait or ask the user; they can also archive it from the sidebar.

At that moment three candidate conditions were all true — the session was pinned, it was mid-turn (unavoidable when archiving "self"), and the user had it open on screen — and (pinned or in use) separates none of them.

Two things are wrong here.

1. The message discards a reason the app already computed.

agentArchiveHoldFor in the shipped bundle computes a four-value reason:

let a = i === "turn" || (!r && e.isRunning) ? "running"
      : i === void 0
        ? (this.isPossiblyPinned(e.sessionId) ? "pinned"
           : (!r && this.isSessionKnownVisible(e.sessionId) ? "on_screen" : void 0))
        : "losable_work";

The message builder renders only three branches, so pinned and on_screen collapse into one string:

e.reason === "running"        ? `${r} is still working (a turn in progress)`
: e.reason === "losable_work" ? `${r} still has live work (an agent run, a Remote Control client, a queued message or a background task)`
: `the app is keeping ${…} for the user (pinned or in use)`

The suggested remedy compounds it: "Wait or ask the user" is right for on_screen and wrong for pinned, because waiting never clears a pin. An agent that reads this message will retry against a condition that cannot expire.

For the record, in the case above the blocker was provably the pin: on_screen is guarded by !r (r = "target is the calling session"), so it cannot fire for a self-archive, and the message rendered "it" rather than "its side session …". isRunning: true did not block it — losableWorkKind(id, {beyondOwnTurn: true}) skips the in-flight-turn check, which is what makes session_id: "self" usable mid-turn at all.

2. The pin's documented scope does not match where it is enforced.

The ccd_sidebar__set_pinned tool description says a pinned session "is shown under Pinned above every group and is kept out of automatic archiving" (emphasis on automatic is mine). The auto-archive engine agrees, holding sweeps with the reason string "it is pinned — a pin is an explicit ‘keep this visible’".

But the same hold is applied to archive_session, which is not automatic. Per its own description, "The app asks the user to approve each call (in auto mode it may approve without asking …)". So a user-approved lifecycle command is refused on the grounds that "the app is keeping it for the user", and the tool description that defines what a pin does says nothing about that.

What should happen

For 1, the two reasons should render separately — this is worth doing whatever is decided about 2:

  • pinned → … was not archived: it is pinned in the sidebar. Ask the user to unpin it, or to archive it from the sidebar.
  • on_screen → … was not archived: the user has it open on screen. Try again once they move away, or ask them.

For 2, our position is that a pin is a sidebar-placement preference and archiving is an explicit lifecycle command, so an approved archive_session should drop the pin and proceed. We recognise that is a policy call and that the opposite has been asked for — #62104 requested exactly this guard (it was closed by the stale bot on 2026-08-04 without a maintainer response; we make no claim about whether it drove the current behaviour). If the guard is intended, then the set_pinned description should be widened, because "kept out of automatic archiving" does not describe what it currently does.

Steps to reproduce

  1. Pin a Code session in the Desktop sidebar.
  2. From that session, call mcp__ccd_session_mgmt__archive_session with session_id: "self".
  3. The call is refused with the message above.
  4. mcp__ccd_session_mgmt__get_session with session_id: "self" reports pinned: true.

The refusal text itself points at the sidebar as the way out — "they can also archive it from the sidebar". I have not tested that path, so take it on the app's authority rather than mine.

What is measured is the guard's reach: agentArchiveHoldFor has two call sites in the bundle, both agent-initiated — completeAgentArchive, which archiveSessionForAgent reaches through, and the archive_session MCP handler — and archiveSession() itself is a thin wrapper over teardownSession(e, "archive", …) with no pin check on it. That is not a claim that the pin is consulted in only one place: it also gates the auto-archive hold, a device-storage cleanup skip, and whether a pinned side session cascades with its parent.

Related

  • #62104 — feature request for exactly this guard (closed stale, no maintainer response).
  • #91720 and #90757 — requests for a set_pinned write tool. Relevant here because the ccd_sidebar tools were not exposed to the session in this build, so an agent that hits this refusal has no in-band way to unpin and can only stop and ask. If the pin hold is kept, exposing set_pinned alongside archive_session would at least make the refusal recoverable.

Additional information

Minor related observation, same root cause on the actionability side: isStarPinnedForPool treats an unknown star state as pinned while star sync is pending, for up to 10 minutes (6e5 ms) after app init or a star-sync epoch reset. In that window an unpinned session can be refused with the same "pinned or in use" message. Failing closed is defensible; being unable to distinguish it from a real pin is the problem described in 1.

Note for anyone searching the bundle: the refusal sentence never appears as a literal, because it is assembled from a template with ${…} interpolation. Grepping for the app is keeping it for the user finds nothing; pinned or in use finds it.

Environment

  • Claude Desktop 1.49585.0 (macOS 26.6, build 25G72, arm64)
  • Claude Code CLI 2.1.236 (Claude Code)
  • Platform: Anthropic API
  • Model: Opus
  • Regression: unknown — we have not tested an earlier Desktop build
  • Surface: Claude Desktop Code tab (not a terminal session)

---

Edit (2026-09-10): corrected three claims in the original text — agentArchiveHoldFor has two call sites, not three (the third grep hit was the method definition); the sentence saying the hold "lives only in" that function was dropped, because a pin also gates the auto-archive hold, a cleanup skip and side-session cascade; and the sentence asserting the sidebar workaround "works" now says plainly that it is the app's claim and not something I tested. The findings themselves are unchanged. (A first version of this note said "two claims" and undercounted itself.)

View original on GitHub ↗