Skip to content

fix(plugin): pin cache-hit MCP placeholder tools for the whole session#6105

Merged
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/mcp-toolset-session-pinning
Jul 6, 2026
Merged

fix(plugin): pin cache-hit MCP placeholder tools for the whole session#6105
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/mcp-toolset-session-pinning

Conversation

@SivanCola

Copy link
Copy Markdown
Collaborator

Summary

MCP 工具集会话内固定——"高缓存率"专项的最后一块。provider 请求的 tools 数组是缓存前缀的一部分,cache_shape.go 早已把会话中途的工具变化记为 "tools" 前缀失效(整段对话 10 倍 miss 计价冷启动)。

现状调查结论:lazy placeholder 机制其实已经完成了 90% 的工作——cache-hit 时占位符携带完整缓存 schema(与真实工具走同一个 provider.CanonicalizeSchema,无 drift 时逐字节相同),boot 时即注册。问题出在最后一步:后台握手一完成,trySwap 就把注册表里的占位符替换成 live 工具。只要 live 握手与缓存有任何差异(描述微调、schema 升级、server 新增工具),tools 数组就在会话中途改字节 → 前缀全灭。

修复(最小改动):

  • cache-hit 占位符会话内固定trySwap 不再重写注册表。占位符从 boot 起呈现的缓存字节保持到会话结束,Execute 通过共享 spawn 状态转发到真实工具——这条转发路径本来就存在(lazy.go 的 spawnReady 分支),swap 从来不是功能必需的。live 差异照旧写入 schema 缓存(saveLazyCachedSchema),下一个会话呈现新表面——与 fix(environment): persist probe snapshots so the prompt prefix stays byte-stable #6093 环境探测快照同一个取舍:新鲜度延迟一个会话,换会话内字节稳定。
  • cache-miss connect stub 保留 swap:首次使用某 server 时没有任何真实 schema 可呈现,swap 是一次性成本,且后台 kick 通常赶在用户首条消息之前完成。
  • 边角修复:缓存里 0 个工具的快照现在也走 stub 路径——此前这种情况 live 工具会在会话中途无占位地加入注册表(也是漂移源)。
  • 覆盖所有 trySwap 调用方,包括 IsServerAlreadyConnected 共享 host 恢复路径(修改在函数内部区分,天然覆盖)。

行为边界(review 时请关注):live 握手缺少缓存中某工具时,该占位符调用报 "did not expose tool (cached schema may be stale)"——与修复前行为一致(旧 swap 同样不移除消失的 cache-hit 占位符);live 新增的工具本会话不可见,下会话出现。

Verification

  • 新增 TestLazyCacheHitPinsToolBytesAcrossDivergentHandshake:缓存故意与 live 分歧(过期描述/schema + 缺一个工具),后台握手完成并 Execute 后,注册表 Schemas() 字节与 boot 时完全一致、live-only 工具未中途加入、刷新后的磁盘缓存已含 live 真集。反向验证:恢复旧 swap 行为后该测试失败。
  • 新增 TestLazyEmptyCachedToolsFallsBackToConnectStub;更新 TestLazyCacheHitSyncSpawn(断言从"swap 成 remoteTool"改为"占位符保留 + schema 字节不变 + 二次调用转发成功")。
  • internal/plugin(含 -race)、internal/bootinternal/controlinternal/agent、desktop 全套(37s)全绿;go vet 干净。

Cache impact

Cache-impact: high - this IS a cache-stability fix: removes the mid-session tools-array rewrite class of prefix invalidation. Tool surface may lag live server state by one session on cache-hit drift.
Cache-guard: TestLazyCacheHitPinsToolBytesAcrossDivergentHandshake asserts registry schema bytes are identical before/after a divergent handshake; reverse-verified against the old swap behavior.
System-prompt-review: N/A (tools array, not system prompt; no prompt content changes)

Refs #2945, #5614; completes the byte-stability line (#6093 system prompt, #6094 request prefix, this PR tools array).

The provider request's tools array is part of the cached prompt prefix,
and cache_shape.go already tracked mid-session tool changes as a "tools"
prefix invalidation (a full-conversation cache miss at 10x pricing).
The lazy-placeholder mechanism registered byte-stable cached schemas at
boot — and then trySwap replaced them with the live tools as soon as
the background handshake finished. Whenever the live handshake differed
from the cache (description tweaks, schema upgrades, newly added
tools), the swap rewrote the tools array mid-session and cold-started
the conversation's provider cache.

Pin cache-hit placeholders instead: trySwap no longer touches the
registry for them. The lazyTools keep presenting the cached bytes the
model has seen since boot and forward Execute to the real tools through
the shared spawn state (which already worked — the swap was never
functionally necessary). Live drift still lands in the schema cache via
saveLazyCachedSchema, so the NEXT session presents the updated surface:
freshness deferred one session for byte-stable tool bytes within this
one — the same trade the environment-probe snapshot makes for the
system prompt.

The cache-miss connect stub still swaps (there was nothing real to
present); that is a one-time cost per server, and the background kick
usually completes before the first user message anyway. A snapshot with
zero cached tools now also takes the stub path — previously its live
tools joined the registry mid-session with no placeholders at all.

Covers all trySwap callers including the IsServerAlreadyConnected
shared-host recovery paths. Verified by reverting the guard: the new
divergent-handshake test fails with the old swap behavior.
@SivanCola SivanCola requested a review from esengine as a code owner July 6, 2026 13:16
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development mcp MCP servers / plugins (internal/plugin, codegraph) labels Jul 6, 2026
…ble one

macOS CI caught a race in the new divergent-handshake test: the stale
cache the test writes is itself loadable, so waitForCachedSchema could
return it before the background saveLazyCachedSchema landed. Poll until
the cache actually contains the live tool set (bounded by the same 5s).
@SivanCola SivanCola merged commit 7ae4219 into esengine:main-v2 Jul 6, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcp MCP servers / plugins (internal/plugin, codegraph) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant