Skip to content

feat: 拦截 /compact 与 /clear,让上下文压缩真正生效#263

Merged
lishuceo merged 5 commits into
mainfrom
feat/claude-session-b59911
Jun 15, 2026
Merged

feat: 拦截 /compact 与 /clear,让上下文压缩真正生效#263
lishuceo merged 5 commits into
mainfrom
feat/claude-session-b59911

Conversation

@lishuceo

Copy link
Copy Markdown
Owner

背景

用户反馈在飞书直接发 /compact 没有任何效果——上下文一直涨不下来,单次对话动辄十几刀。

根因(已从运行日志确认)

抓到实际执行记录:用户发的 text: "/compact"(8 字符),但丢给 SDK 的 promptLength283 字符,在 77.6 万 token 上下文里 resume,跑了一个普通对话轮次(输出 796 token 文字),上下文丝毫未减。

三层原因:

  1. anycode 没拦截 /compact —— 本地命令清单只有 /auth /t /status /reset /stop /edit /dev /fork /memory /config /help/compact 直接 fall-through 到普通任务。
  2. 普通任务链路层层加前缀 —— 发送者标签 [姜黎]:<msg-time>、聊天历史、memory/bot 身份,把 /compact 挤出消息开头。
  3. CLI 只在消息首字符为 / 时识别 local slash command —— 被挤到中间后,/compact 被当成纯文本。

改动

  • 新增 ClaudeExecutor.compact():以 "/compact" + resume 调 SDK,捕获 compact_boundary(pre/post tokens) 与 status(compact_result/error)。
  • handleSlashCommand 拦截 /compact:取 thread/主 session 的 conversationId 作 resume 目标,busy 守卫,压缩后回存 session ID 并回复压缩比。
  • /clear 作为 /reset 别名;/help 增列两命令。

实测验证(SDK 0.3.156,真实 API)

写探针脚本跑了真实「建会话 → /compact」:

  • /compact 确实被识别为 local 命令并执行(非文本)。
  • 消息序列:status(compacting)status(compact_result+error)initassistantresult
  • 成功压缩 40445 → 1351 tokens(↓97%),耗时 35s。
  • session_id 压缩前后保持不变 → resume 同一 id 即继承压缩后上下文。
  • 消息过少时报 Not enough messages to compact(归为 noop,非错误)。

测试

  • executor.test.ts:compact() 成功/noop/失败/无信号 + 断言裸 /compact 调用 + classifyCompactOutcome。
  • compact-reply.test.ts:formatTokenCount / formatCompactReply 各分支。
  • 全量回归 npx vitest run:1700 passed,唯一失败 memory/quality.test.ts(embedding 语义检索环境依赖,已用 git stash 确认在干净 base 上同样失败,与本 PR 无关)。
  • typecheck 0 错,build 通过,lint 0 error。

🤖 Generated with Claude Code

lishuceo and others added 4 commits June 15, 2026 13:53
飞书侧直接发 /compact 无效的根因:normal task 链路会给消息层层加前缀
(发送者标签 [姜黎]:、<msg-time>、聊天历史、memory/bot 身份),把 /compact
挤出消息开头。Claude Code CLI 只在消息首字符为 / 时才识别 local slash command,
于是 /compact 被当成普通文本,模型只回一句话,77 万 token 上下文纹丝不动。

新增 compact() 方法:以**裸** "/compact" 为 prompt + resume 调 SDK,捕获
compact_boundary(pre/post tokens) 与 status(compact_result/error),返回 CompactResult。
实测 SDK 0.3.156:bare /compact 触发真实压缩,session_id 保持不变,
40K→1.4K tokens(↓97%)。classifyCompactOutcome 把 "Not enough messages to
compact" 归为 noop(非错误)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
覆盖:裸 /compact + resume 调用、成功带 pre/post tokens、noop(消息过少)、
真实失败带 error、无 compact 信号时不静默成功。classifyCompactOutcome 各分支。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
handleSlashCommand 新增 /compact 块:取 thread/主 session 的 conversationId
作 resume 目标,busy 守卫,调 claudeExecutor.compact,成功后回存 session ID,
回复 token 压缩比。/clear 作为 /reset 别名(本地清 conversationId 即等效清空)。
/help 增列两命令。导出纯函数 formatCompactReply + formatTokenCount。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
formatTokenCount(紧凑 k 表示)与 formatCompactReply(成功带压缩比/纯成功/
noop/失败各分支)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Reviewing PR #263 — 拦截 /compact 与 /clear

  • Read CLAUDE.md & PR intent
  • Check for previous review comments
  • Review src/claude/executor.ts (compact())
  • Review src/feishu/event-handler.ts (slash handling)
  • Review src/claude/types.ts
  • Review tests
  • Post review summary

View job run

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 5m 36s —— View job


Review: PR #263 — 拦截 /compact 与 /clear

⚠️ Issues Found — 整体设计扎实(裸 /compact 透传、信号分类、graceful noop、测试覆盖都到位),但有一处会破坏会话隔离的 bug,建议合并前修掉。

🔴 话题内 /compact 覆盖主会话 conversationId(confidence 85)

event-handler.ts:1192-1197。读取 resume 目标用 if/else(话题用 thread session,否则用主 session),但回存时 setConversationId(主会话)无条件执行。话题内压缩时,会把话题的会话 id + 话题 cwd 写进主会话,导致用户回主聊天框发消息时 resume 到话题会话、用错工作区,跨话题串味。已附 inline 修复建议(把主会话回存移入 else)。Fix this →

🟡 压缩期间并发:普通消息会与 compact 并发 resume 同一 session(confidence 75)

/compacthandleSlashCommand同步执行(约 35s),不走 taskQueue;而 taskQueuequeue.ts)只按 queueKey 串行,不读 session.status。busy 守卫只挡住了「任务运行时禁止 compact」这一正向,没挡住反向——compact 进行中的 35s 里,用户再发一条普通消息会被 enqueue 并立即 resume 同一个 conversationId(compact 尚未回存,id 不变),两个子进程并发 resume 同一 session,可能丢失压缩结果或写坏 session 文件。

其它 slash 命令都是瞬时的,/compact 是第一个在队列外做 35s 重量级会话操作的命令,所以这是新引入的窗口。建议:让 /compact 也走 taskQueue(入队串行),或在出队前检查 session.status

备注(非阻塞)

  • busy 守卫用的是 session.status 检查 + setStatus 的非原子 check-then-set;仓库里已有原子的 sessionManager.tryAcquire()(注释明说为防 TOCTOU)。当前写法与 normal flow 一致,可接受,但若改走队列方案可一并收敛。
  • PR 描述的真实消息序列未提到 compact_boundary;若生产环境某些 resume 流确实不发 compact_boundarypreTokens/postTokens 会缺失,回复优雅降级为「✅ 上下文已压缩。」(无压缩比)。非 bug,仅提示压缩比可能不总是显示。

✅ 做得好的地方

  • classifyCompactOutcome 拆成纯函数 + 把「Not enough messages」归为 noop 而非 error,分类清晰、可测。
  • /compact 的根因分析准确,单测里直接断言 prompt === '/compact' 守住了回归。
  • compact() 复用了 idle-timeout / runningQueries 注册(可被 /stop kill)模式,env 展开、cwd 回退都处理到位。
  • 测试覆盖 success / noop / error / 无信号 + formatTokenCount/formatCompactReply 各分支,符合 CLAUDE.md「feat 必带 test」。

注:未能运行 npm run typecheck / npx vitest(命令需审批),以上基于静态审查;PR 描述称全量回归 1700 passed。
· feat/claude-session-b59911

@lishuceo lishuceo merged commit 4a36dca into main Jun 15, 2026
4 checks passed
Comment on lines +1192 to +1197
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
}
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: 话题内 /compact 会用「话题会话 id」覆盖「主会话 id」,破坏 thread 隔离 (confidence: 85)

读取 resume 目标时是 if/else:话题内用 thread session,否则用主 session。但回存时 setThreadConversationId 是有条件的,setConversationId(主会话)却无条件执行。

effectiveThreadId 存在时:

  • conversationId 来自 thread session(话题的会话),压缩后 result.sessionId话题的会话 id;
  • 第 1196 行把这个话题会话 id + 话题的 cwd 写进了主会话chatId:userId 级别)。

后果:用户回到主聊天框(非话题)发消息时,主会话会 resume 到话题的会话、用错 cwd(话题工作区),跨话题串味,正是架构要防的隔离破坏。主会话自身的 conversationId 当时并未被压缩,本不该被改写。

修复:把主会话回存放进 else 分支,与读取逻辑对称:

Suggested change
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
}
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
} else {
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}
}

Comment on lines +1192 to +1197
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
}
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: 话题内 /compact 会用「话题会话 id」覆盖「主会话 id」,破坏 thread 隔离 (confidence: 85)

读取 resume 目标时是 if/else:话题内用 thread session,否则用主 session。但回存时 setThreadConversationId 是有条件的,setConversationId(主会话)却无条件执行。

effectiveThreadId 存在时:

  • conversationId 来自 thread session(话题的会话),压缩后 result.sessionId话题的会话 id;
  • 第 1196 行把这个话题会话 id + 话题的 cwd 写进了主会话chatId:userId 级别)。

后果:用户回到主聊天框(非话题)发消息时,主会话会 resume 到话题的会话、用错 cwd(话题工作区),跨话题串味,正是架构要防的隔离破坏。主会话自身的 conversationId 当时并未被压缩,本不该被改写。

修复:把主会话回存放进 else 分支,与读取逻辑对称:

Suggested change
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
}
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}
if (result.success && result.sessionId) {
if (effectiveThreadId) {
sessionManager.setThreadConversationId(effectiveThreadId, result.sessionId, cwd, agentId);
} else {
sessionManager.setConversationId(chatId, userId, result.sessionId, cwd, agentId);
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant