anthropics / anthropics/claude-code
[BUG] archive_session refuses a pinned session, and the refusal can't say which of four conditions blocked it
- 主要语言
- Python
- 星标
- 145k
- 派生
- 23.1k
- PR 合并指标
- PR 指标待抓取
描述
## 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.)
贡献指南
这个仓库没有索引到贡献指南
调研方向
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.
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- javascript
- 领域
- api, backend
- Issue 类型
- 缺陷
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 35/100