[BUG] archive_session refuses a pinned session, and the refusal can't say which of four conditions blocked it
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
- Pin a Code session in the Desktop sidebar.
- From that session, call
mcp__ccd_session_mgmt__archive_sessionwithsession_id: "self". - The call is refused with the message above.
mcp__ccd_session_mgmt__get_sessionwithsession_id: "self"reportspinned: 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_pinnedwrite tool. Relevant here because theccd_sidebartools 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, exposingset_pinnedalongsidearchive_sessionwould 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.)