[Bug] Manual /compact on a resumed session summarizes through the session's stale routed provider, with zero cache reuse #3565
Replies: 2 comments
|
你这个观察和 #3542 是同一块代码的不同入口:#3542 是 compaction 启动后被 caller 中止(5-15s 稳定超时),你这里是 |
|
zoahdev 的 1. 这是"persisted snapshot vs live state"根因族的第 3 个成员 这个 bug 与 subagent 模型路由继承(#3552,父会话切 provider 后子代理继承冻结的
共同教训:"上次成功路由"作为默认值是合理的 warm-cache 启发,但它一旦可能跨天/跨会话存活,就必须有 freshness 守卫。 2. 修复分级建议(比"加 currentRoute"再多一步)
3. 测试盲区确认(你点到的)
家族状态:subagent(#3552)+ compaction(#3565)= "snapshot vs live" 2 报告;修复形态共享"注入 live source"原则。已建家族记忆跟踪。 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
compaction-basicresolves its summarization target asThat ranks a persisted, arbitrarily old request header above the agent's currently configured provider. The rationale for preferring it is prefix-cache reuse, stated right above the call:
That premise does not hold for a manual
/compacton a resumed session. There is no turn in flight, the header can be days old, and the provider's cache is long gone — so the call inherits the old provider's billing without any of the reuse it was inherited for.What happened
A session was last active on 2026-08-14; its final
request/headerrecordedprovider: deepseek-official. The provider in use changed afterwards. On 2026-08-20 the session was reopened and/compactwas run before any new turn:Two fields carry the whole report.
turn: null— there is no request in flight, so there is no current route at all.session.requestHeader()can only return the six-day-old one. Preferring "the latest routed header" degenerates to "whatever provider this session happened to end on, whenever that was".cacheReadTokens: 0— the reuse that justified inheriting that provider never happened. 375,320 input tokens, none of them cached, billed to a provider the account had moved off. The next real turn, 70 seconds later, loggedprovider: ark.The first signal that any of this had happened was a billing dashboard the following hour. Nothing in the session, the command result, or the UI mentions which provider a compaction used.
The cache miss was predictable, not unlucky
Inheriting the routed header is a bet that the provider still holds the conversation's prefix. That bet is a good one in the common case — a compaction fired while the session is active replays a prefix the provider served minutes ago, and the reuse is real. It is what the design is for.
But whether the bet can pay is knowable before the call, from data the harness already has: the age of the header it is about to inherit. A session reopened after days is past any provider's prefix-cache TTL, so the reuse is not merely unlikely, it is arithmetically impossible — and inheriting the route then buys nothing while still deciding who gets billed.
turn: nullis the same signal from the other side: no request is in flight, so there is no warm prefix to be continuing from in the first place.So this is not "the cache sometimes misses". It is a bet placed in the one situation where it is already known to be lost.
Why this is not #1944
#1944 reports that the summarizer inherits too little of the routed header — it drops
reasoningEffortand adds an explicitmaxTokens, splitting the cache key — and proposes inheriting the header config wholesale. That treats "the last routed header" as the correct source of truth and makes the inheritance more complete.In the resumed-session case that cuts the wrong way: the whole of a stale header would be carried into the summarization call rather than two fields of it.
Both readings can be right at once. Inherit the header faithfully when it is current; don't inherit it at all when it plainly isn't.
Suggested fixes
1. Gate
lateston freshness, and fall through toagentTargetwhen it fails. Two cheap tests, both from data already at hand:turn: nullatcompaction/start— no request in flight, so there is no warm prefix to continue from, and the rationale forlatestcannot apply by construction.turnis not null but the session may still have been idle for days.When either fails,
agentTargetis the better default: it is at least the provider the user believes they are on, and nothing is given up, because the reuse thatlatestwas preferred for was not available anyway.2. Say where the summary went.
command/donecurrently readsCompacted 419 history items (~237097 tokens).and names no provider. Appendingvia <provider>/<model>would surface this at the moment it happens rather than on a bill. This one is cheap and worth having on its own merits: today a/compactcan spend real money on a provider the user believes is inactive, and nothing says so.Notes
summarizationProvider/summarizationModelin the plugin config outranklatest, so pinning them is a workaround.tests/compaction-basic.spec.ts— "resolves the latest routed provider/model before the AgentOptions pair". That test appends the header inside the same session withreason: 'initial', i.e. a warm route. No test covers a header that is stale relative to the agent's current configuration.agent.options.providerwas at the instant/compactran, so I am not claiming a specific correct provider was ignored — only that a six-day-old header was preferred withturn: nulland returnedcacheReadTokens: 0.0.1.0-rc.8.All reactions