anthropics / anthropics/claude-code
[BUG] archive_session refuses a pinned session, and the refusal can't say which of four conditions blocked it
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
## 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 ("") 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:
```js
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:
```js
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.)
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start at the shipped bundle's agentArchiveHoldFor function and its two call sites, completeAgentArchive and the archive_session MCP handler. Reproduce the pinned self-archive case and compare the computed reason with the rendered refusal; done means pinned and on_screen are distinguishable, with the pin-enforcement policy and set_pinned description aligned.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript
- Ambito
- api, backend
- Tipo di issue
- Bug
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 35/100