Skip to content

fix: 为 sessionStorage existingSessionFiles Map 添加容量上限#1227

Open
Evsdrg wants to merge 3 commits into
claude-code-best:mainfrom
Evsdrg:fix/session-files-cache-leak
Open

fix: 为 sessionStorage existingSessionFiles Map 添加容量上限#1227
Evsdrg wants to merge 3 commits into
claude-code-best:mainfrom
Evsdrg:fix/session-files-cache-leak

Conversation

@Evsdrg
Copy link
Copy Markdown
Contributor

@Evsdrg Evsdrg commented May 14, 2026

Summary

existingSessionFiles Map 缓存 sessionId → 文件路径映射以避免重复
stat 调用,但在 coordinator/swarm 模式下,每个子代理产生独立
sessionId,长时间运行的 daemon 会话可能累积数千条目导致内存泄漏。

新增 MAX_CACHED_SESSION_FILES = 200 上限,新增条目时若达到上限则
逐出最早插入的条目(利用 Map 按插入顺序迭代的特性)。

同时在 _resetFlushState() 中清除此缓存以保证测试隔离。

Test plan

  • tsc --noEmit 零错误
  • biome format 零差异
  • biome lint 零违规

🤖 Generated with Claude Code Best

Summary by CodeRabbit

  • Bug Fixes

    • Fixed potential unbounded memory growth in cache tracking and session storage during long-running daemon/swarm operations.
  • Performance

    • Improved token aggregation to include local-agent task results.
    • Enhanced structured result tracking for in-process teammate executions.

Review Change Stack

Evsdrg and others added 3 commits May 14, 2026 15:40
Spinner.tsx 的 token 聚合循环仅统计 in_process_teammate 类型任务,
漏掉了 local_agent(后台代理/verification agent)类型。当后台代理
运行时,主界面 spinner 一直显示 "↓ 0 tokens",因为 background agent
的 token 消耗未被纳入 teammateTokens 聚合。

同时在 inProcessRunner.ts 中,进程内队友完成时计算并设置 result
(含 totalTokens/totalToolUseCount/content/usage),使详情弹窗可以
正确展示累计 token 消耗,不再仅依赖 progress.tokenCount 间歇更新。

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
Map 以 querySource 为 key 存储每个来源的缓存命中率历史状态,
但 querySource 类型为 `any`,长时间会话中可能产生大量唯一值,
Map 持续增长永不清理。

新增 MAX_SOURCE_ENTRIES = 50 上限,新增条目时若达到上限则
逐出最早插入的条目(Map 按插入顺序迭代)。

同时也新增 _resetCacheWarningStateForTest() 用于测试隔离。

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
existingSessionFiles Map 缓存 sessionId → 文件路径映射以避免重复
stat 调用,但在 coordinator/swarm 模式下,每个子代理产生独立
sessionId,长时间运行的 daemon 会话可能累积数千条目。

新增 MAX_CACHED_SESSION_FILES = 200 上限,新增条目时若达到上限则
逐出最早插入的条目。同时在 _resetFlushState() 中清除此缓存以保证
测试隔离。

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR adds memory bounding safety improvements across two utilities, captures structured execution results for in-process teammate runs with duration and token accounting, and expands the spinner UI to aggregate tokens from both in-process and local-agent tasks.

Changes

Memory Bounding Improvements

Layer / File(s) Summary
Cache warning state bounding
src/utils/cacheWarning.ts
Introduces MAX_SOURCE_ENTRIES cap at 50 distinct query sources, evicts oldest entries on overflow, and adds _resetCacheWarningStateForTest() helper for test isolation.
Session file cache bounding
src/utils/sessionStorage.ts
Introduces MAX_CACHED_SESSION_FILES cap at 200, evicts oldest session keys when cache reaches capacity, integrates reset into _resetFlushState(), and updates getExistingSessionFile documentation.

Teammate Execution Tracking and Reporting

Layer / File(s) Summary
In-process teammate result computation
src/utils/swarm/inProcessRunner.ts
Captures startTime at run entry, walks backward through allMessages to extract last assistant text, count tool\_use blocks, and derive token totals from assistant usage, then constructs and attaches AgentToolResult with duration and usage to final task state.
Expand spinner token aggregation
src/components/Spinner.tsx
Imports isLocalAgentTask and updates teammate token sum condition to include running local-agent tasks alongside in-process teammate tasks.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Memories bound and safe from spill,
Swarms run long and never will
Grow unbounded through the night,
Results captured, tokens bright!
Local agents, spinners too—
All accounted for, tried and true!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes adding a capacity limit to sessionStorage's existingSessionFiles Map, which is only one of four changes in this PR. The PR also includes memory leak fixes for cacheWarning state tracking, token aggregation improvements for local-agent tasks in Spinner, and structured result captures in inProcessRunner. Revise the title to reflect the primary/main change across all modified files, or if sessionStorage is intended as the main focus, consider consolidating related changes into a separate PR to align scope with the title.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/swarm/inProcessRunner.ts`:
- Around line 1472-1511: The loop uses "as any[]" for message blocks; replace
that with a safe typed form and runtime type guards: cast m.message?.content to
unknown[] or unknown and then narrow it (e.g., const blocksUnk =
m.message?.content ?? []; treat blocksUnk as unknown[] and validate each entry
with typeof checks or by asserting it as Record<string, unknown> before
accessing .type/.text), update the variables that depend on those fields
(blocks, textBlocks, lastAssistantContent) to use the narrowed types, and if a
static cast is required use the double assertion pattern (unknown as
SpecificType[]) rather than "as any"; keep AgentToolResult usage and
getTokenCountFromUsage calls unchanged but ensure lastUsage and completionTokens
derive from the properly typed m.message.usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73575530-2c56-4a34-9555-d125b33ce0d9

📥 Commits

Reviewing files that changed from the base of the PR and between 3d7b32f and 1155c86.

📒 Files selected for processing (4)
  • src/components/Spinner.tsx
  • src/utils/cacheWarning.ts
  • src/utils/sessionStorage.ts
  • src/utils/swarm/inProcessRunner.ts

Comment on lines +1472 to +1511
// Compute result so the detail dialog can show token usage.
// Walk backwards for the last API usage (cumulative input_tokens from the
// Anthropic API already includes all prior context).
let completionTokens = 0
let completionToolUseCount = 0
let lastAssistantContent: AgentToolResult['content'] = []
let lastUsage: AgentToolResult['usage'] | undefined
for (let i = allMessages.length - 1; i >= 0; i--) {
const m = allMessages[i]!
if (m.type === 'assistant') {
const blocks = (m.message?.content ?? []) as any[]
for (const b of blocks) {
if (b?.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter((b: any) => b?.type === 'text')
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map((b: any) => ({
type: 'text' as const,
text: b.text,
}))
}
if (!lastUsage && m.message?.usage) {
lastUsage = m.message.usage as AgentToolResult['usage']
completionTokens = getTokenCountFromUsage(
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
)
}
if (completionTokens > 0 && lastAssistantContent.length > 0) break
}
}

const teammateResult: AgentToolResult = {
agentId: identity.agentId,
agentType: 'teammate',
content: lastAssistantContent,
totalToolUseCount: completionToolUseCount,
totalDurationMs: Date.now() - startTime,
totalTokens: completionTokens,
usage: lastUsage as AgentToolResult['usage'],
} as unknown as AgentToolResult
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace as any with proper type handling.

Line 1482 uses as any[] which violates the coding guidelines. The guidelines prohibit as any in production code and recommend using double assertion or proper type guards instead.

As per coding guidelines, "Prohibit as any type assertions in production code; in test files, as any is permitted for mock data. Use as unknown as SpecificType double assertion or interface supplementation when type mismatches occur" and "Use Record<string, unknown> instead of any for objects with unknown structure".

🔧 Proposed fix using proper type handling
-    const blocks = (m.message?.content ?? []) as any[]
+    const blocks = (m.message?.content ?? []) as unknown as Array<Record<string, unknown>>
     for (const b of blocks) {
-      if (b?.type === 'tool_use') completionToolUseCount++
+      if (typeof b === 'object' && b !== null && b.type === 'tool_use') completionToolUseCount++
     }
-    const textBlocks = blocks.filter((b: any) => b?.type === 'text')
+    const textBlocks = blocks.filter((b): b is Record<string, unknown> => 
+      typeof b === 'object' && b !== null && b.type === 'text'
+    )
     if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
-      lastAssistantContent = textBlocks.map((b: any) => ({
+      lastAssistantContent = textBlocks.map(b => ({
         type: 'text' as const,
-        text: b.text,
+        text: typeof b.text === 'string' ? b.text : '',
       }))
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Compute result so the detail dialog can show token usage.
// Walk backwards for the last API usage (cumulative input_tokens from the
// Anthropic API already includes all prior context).
let completionTokens = 0
let completionToolUseCount = 0
let lastAssistantContent: AgentToolResult['content'] = []
let lastUsage: AgentToolResult['usage'] | undefined
for (let i = allMessages.length - 1; i >= 0; i--) {
const m = allMessages[i]!
if (m.type === 'assistant') {
const blocks = (m.message?.content ?? []) as any[]
for (const b of blocks) {
if (b?.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter((b: any) => b?.type === 'text')
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map((b: any) => ({
type: 'text' as const,
text: b.text,
}))
}
if (!lastUsage && m.message?.usage) {
lastUsage = m.message.usage as AgentToolResult['usage']
completionTokens = getTokenCountFromUsage(
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
)
}
if (completionTokens > 0 && lastAssistantContent.length > 0) break
}
}
const teammateResult: AgentToolResult = {
agentId: identity.agentId,
agentType: 'teammate',
content: lastAssistantContent,
totalToolUseCount: completionToolUseCount,
totalDurationMs: Date.now() - startTime,
totalTokens: completionTokens,
usage: lastUsage as AgentToolResult['usage'],
} as unknown as AgentToolResult
// Compute result so the detail dialog can show token usage.
// Walk backwards for the last API usage (cumulative input_tokens from the
// Anthropic API already includes all prior context).
let completionTokens = 0
let completionToolUseCount = 0
let lastAssistantContent: AgentToolResult['content'] = []
let lastUsage: AgentToolResult['usage'] | undefined
for (let i = allMessages.length - 1; i >= 0; i--) {
const m = allMessages[i]!
if (m.type === 'assistant') {
const blocks = (m.message?.content ?? []) as unknown as Array<Record<string, unknown>>
for (const b of blocks) {
if (typeof b === 'object' && b !== null && b.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter((b): b is Record<string, unknown> =>
typeof b === 'object' && b !== null && b.type === 'text'
)
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map(b => ({
type: 'text' as const,
text: typeof b.text === 'string' ? b.text : '',
}))
}
if (!lastUsage && m.message?.usage) {
lastUsage = m.message.usage as AgentToolResult['usage']
completionTokens = getTokenCountFromUsage(
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
)
}
if (completionTokens > 0 && lastAssistantContent.length > 0) break
}
}
const teammateResult: AgentToolResult = {
agentId: identity.agentId,
agentType: 'teammate',
content: lastAssistantContent,
totalToolUseCount: completionToolUseCount,
totalDurationMs: Date.now() - startTime,
totalTokens: completionTokens,
usage: lastUsage as AgentToolResult['usage'],
} as unknown as AgentToolResult
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/swarm/inProcessRunner.ts` around lines 1472 - 1511, The loop uses
"as any[]" for message blocks; replace that with a safe typed form and runtime
type guards: cast m.message?.content to unknown[] or unknown and then narrow it
(e.g., const blocksUnk = m.message?.content ?? []; treat blocksUnk as unknown[]
and validate each entry with typeof checks or by asserting it as Record<string,
unknown> before accessing .type/.text), update the variables that depend on
those fields (blocks, textBlocks, lastAssistantContent) to use the narrowed
types, and if a static cast is required use the double assertion pattern
(unknown as SpecificType[]) rather than "as any"; keep AgentToolResult usage and
getTokenCountFromUsage calls unchanged but ensure lastUsage and completionTokens
derive from the properly typed m.message.usage.

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