Skip to content

Commit 66e70b6

Browse files
committed
fix(runner): keep the warm session when the client sends a minimal history
The keep-alive check fingerprints the prior conversation the client sent and compares it against what the previous turn stored. A last-message-only client sends no prior conversation, so the fingerprint is of an empty array and can never match. Every conversation was evicted to cold on every turn after the first. Measured on a three-turn run, turn 2 went from 1570ms to 4623ms while the suite still reported a pass, because a cold turn is still faster than the first. Skip the history comparison when the request carries only its own fresh user turn. The session id already binds the request to the conversation; a minimal-history client simply no longer asserts it, so there is nothing to compare. Requests that do send a history are still checked exactly as before, including the approval-resume path, which always sends the full history. Claude-Session: https://claude.ai/code/session_01KM69J7uHafgciiN5zfG7qR
1 parent b652316 commit 66e70b6

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

services/runner/src/server.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
computeCredentialEpoch,
5050
configFingerprint,
5151
credentialEpochMismatch,
52+
carriesMinimalHistory,
5253
mountCredentialsExpired,
5354
expectedNextHistoryFingerprint,
5455
historyFingerprint,
@@ -595,9 +596,15 @@ export async function runWithKeepalive(
595596
existing.credentialEpoch,
596597
incomingEpoch,
597598
);
599+
// A last-message-only client sends no prior conversation, so there is nothing to compare:
600+
// `priorConversation` is empty and the fingerprint can never match what the last turn stored.
601+
// Comparing anyway evicts the warm session on every turn of every conversation. The session
602+
// id already binds the request to this conversation; the client simply no longer asserts it.
603+
const clientAssertsHistory = !carriesMinimalHistory(request);
598604
let mismatch: string | undefined;
599605
if (cfgFp !== existing.configFingerprint) mismatch = "config";
600-
else if (priorFp !== existing.historyFingerprint) mismatch = "history";
606+
else if (clientAssertsHistory && priorFp !== existing.historyFingerprint)
607+
mismatch = "history";
601608
else if (credMismatch) mismatch = credMismatch;
602609
else if (!tailIsFreshUserMessage(request)) mismatch = "tail";
603610

services/runner/tests/unit/session-keepalive-dispatch.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,16 @@ describe("runWithKeepalive: validation mismatches degrade to cold", () => {
476476
assert.equal(calls.acquire, 2);
477477
});
478478

479+
it("a last-message-only turn 2 keeps the warm session (no history to compare)", async () => {
480+
// The flag-on frontend sends ONLY the trailing user message, so `priorConversation` is
481+
// empty and its fingerprint can never match what turn 1 stored. Comparing anyway evicted
482+
// every conversation to cold on every turn.
483+
const minimal = turn2("s1", { messages: [{ role: "user", content: "more" }] });
484+
const { calls, env1 } = await parkThen(minimal);
485+
assert.equal(env1.destroyed, 0, "the warm env must survive a minimal-history turn");
486+
assert.equal(calls.acquire, 1, "no cold re-acquire");
487+
});
488+
479489
it("credential-epoch expiry evicts to cold", async () => {
480490
// The parked mount expiry is in the past, so the next turn's epoch check fails.
481491
const { calls, env1 } = await parkThen(turn2(), {

0 commit comments

Comments
 (0)