anthropics / anthropics/claude-code

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

Aperta
#93,259 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
area:desktop area:mcp bug has repro platform:macos
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

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.