fix(sessions): retry a failed keep-alive write before the expiry and close its window

Merge-time fix for #666. A nudge only goes out inside the last five minutes
before `expiresAt`, so the check armed a whole `KEEP_WARM_LEAD_MS` after a
failed write landed at or past the expiry and stopped with "too close to
expiry": the retry the status promised never happened. The window
`beginKeepAliveTurn()` opened also stayed open for its ten-minute ceiling
with no turn running.

A failed write now ends the window (`endKeepAliveTurn()` joins
`KeepWarmSessionView`; `Session` already has it) and re-arms at
`min(now + KEEP_WARM_RETRY_MS, expiresAt - KEEP_WARM_LAST_CHANCE_MS)`. The
test advances the clock and sees the second write, and a second one pins
that the retry never lands past the last-chance line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-22 19:10:11 +02:00
1 parent 83d80578e4
commit e408fa21bc
5 files changed
+52 -6

No files matched your search

+12 -2
View File
@@ -173,6 +173,8 @@ export interface KeepWarmSessionView {
readonly lastRealPromptAt: number;
isIdle(): boolean;
beginKeepAliveTurn(windowMs: number): void;
/** Close the window `beginKeepAliveTurn()` opened, when the nudge never went out. */
endKeepAliveTurn(): void;
writeViaMux(data: string): Promise<boolean>;
}
@@ -368,8 +370,16 @@ export class PromptCacheKeepWarmService implements KeepWarmScheduler {
this.ports.log?.(
`[KeepWarm] ${ok ? 'nudged' : 'failed to nudge'} ${id.slice(0, 8)} with ${facts.cache?.recacheTokensIfCold ?? '?'} tokens at stake (nudge ${tally.nudges})`
);
// The nudge's own cache report re-schedules; this timer is the fallback if none arrives.
const nextCheckAt = Date.now() + KEEP_WARM_LEAD_MS;
// Sent: the nudge's own cache report re-schedules, and this timer is the fallback if none
// arrives. Not sent: no turn is running, so the window closes now rather than at its
// ceiling, and the retry lands inside the last minutes before the expiry, where a nudge
// can still go out (a lead time later would be past it).
let nextCheckAt = Date.now() + KEEP_WARM_LEAD_MS;
if (!ok) {
session.endKeepAliveTurn();
const expiresAt = facts.cache?.expiresAt ?? 0;
nextCheckAt = Math.min(Date.now() + KEEP_WARM_RETRY_MS, expiresAt - KEEP_WARM_LAST_CHANCE_MS);
}
this.arm(id, nextCheckAt);
this.ports.publish(id, {
...base,