Cache Keepalive: arm per-chat (only on MESSAGE_SENT, disarm on CHAT_CHANGED); fixes false-arming on chat open

cd3be6486e9ca988e7821634e09e245a326e3a56

permissionBRICK <40219477+permissionBRICK@users.noreply.github.com>

2 files changed, +32 -19Ignore whitespace
public/scripts/extensions/keepalive/index.js+31 -18
@@ -52,10 +52,10 @@ const defaultSettings = {
5252
53let idleTimer = null;53let idleTimer = null;
54let keepaliveActive = false;54let keepaliveActive = false;
55// Whether keepalive has been "armed" this browser-tab session. It stays dormant until the55// Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new
56// first real message/generation happens in the tab, because on a fresh load (even with chat56// message in the open chat — on a freshly loaded or switched-to chat (even with history already
57// history already present) we have no way to know whether anything is still cached57// present) we have no way to know whether its prompt is still cached server-side, so pinging would
58// server-side — so pinging before then would be pointless. Resets on page reload.58// be pointless. Reset on page reload and whenever the chat changes.
59let armed = false;59let armed = false;
60// Wall-clock time (Date.now() + delay) the pending idle timer is expected to fire, or null when60// Wall-clock time (Date.now() + delay) the pending idle timer is expected to fire, or null when
61// none is scheduled. Used to detect a timer that fired far too late (sleep/throttle).61// none is scheduled. Used to detect a timer that fired far too late (sleep/throttle).
@@ -103,7 +103,7 @@ function clearIdleTimer() {
103/**103/**
104 * (Re)schedules the keepalive for the ACTIVE profile based on how long it has been since104 * (Re)schedules the keepalive for the ACTIVE profile based on how long it has been since
105 * that profile's last message. Does nothing (and leaves the timer cleared) until keepalive105 * that profile's last message. Does nothing (and leaves the timer cleared) until keepalive
106 * has been armed for this tab session, or when it is disabled for the active connection.106 * has been armed for the current chat, or when it is disabled for the active connection.
107 * Called on activity AND whenever the active profile changes, so each profile keeps its own107 * Called on activity AND whenever the active profile changes, so each profile keeps its own
108 * independent idle clock.108 * independent idle clock.
109 */109 */
@@ -130,15 +130,26 @@ function markActivity() {
130}130}
131131
132/**132/**
133 * Real message/generation activity: arms keepalive for this tab session (if it wasn't already)133 * The user sent a new message in the current chat — the ONLY thing that arms keepalive.
134 * and records the activity. Loading a chat's history does NOT call this.134 * Background/quiet generations, swipes, regenerations, and loading chat history deliberately do
135 * NOT arm it: none of those prove the user is actively continuing THIS chat from a known-cached
136 * prompt (GENERATION_STARTED in particular fires for every quiet/background generation too).
135 */137 */
136function onChatActivity() {138function onMessageSent() {
137 armed = true;139 armed = true;
138 markActivity();140 markActivity();
139}141}
140142
141/**143/**
144 * Opening or switching chats disarms keepalive: a freshly loaded chat is not proof its prompt is
145 * still cached server-side, so we wait for a new sent message in that chat before running again.
146 */
147function onChatChanged() {
148 armed = false;
149 scheduleIdleTimer();
150}
151
152/**
142 * Fires a background ("quiet") generation against the active model to keep the153 * Fires a background ("quiet") generation against the active model to keep the
143 * server-side prompt cache warm. The full prompt prefix (current chat context +154 * server-side prompt cache warm. The full prompt prefix (current chat context +
144 * the keepalive message) is sent — which is what refreshes the cache — while the155 * the keepalive message) is sent — which is what refreshes the cache — while the
@@ -165,7 +176,7 @@ async function fireKeepalive() {
165 // If the timer fired much later than scheduled (machine sleep, background-tab throttling,176 // If the timer fired much later than scheduled (machine sleep, background-tab throttling,
166 // etc.), far more wall-clock time has elapsed than the idle interval, so the cache has177 // etc.), far more wall-clock time has elapsed than the idle interval, so the cache has
167 // almost certainly expired. Don't ping on a stale assumption — go dormant until the next178 // almost certainly expired. Don't ping on a stale assumption — go dormant until the next
168 // real message re-arms keepalive, exactly like a freshly opened tab.179 // sent message re-arms keepalive, exactly like a freshly opened chat.
169 if (scheduledFireTime !== null && (Date.now() - scheduledFireTime) > MAX_TIMER_DRIFT_MS) {180 if (scheduledFireTime !== null && (Date.now() - scheduledFireTime) > MAX_TIMER_DRIFT_MS) {
170 console.debug('[Keepalive] Idle timer fired', Date.now() - scheduledFireTime, 'ms late; skipping and disarming until next activity.');181 console.debug('[Keepalive] Idle timer fired', Date.now() - scheduledFireTime, 'ms late; skipping and disarming until next activity.');
171 armed = false;182 armed = false;
@@ -318,21 +329,23 @@ async function init() {
318 loadSettings();329 loadSettings();
319 setupListeners();330 setupListeners();
320331
321 // A real message/generation arms keepalive for this tab session and counts as using the332 // Only the user sending a new message arms keepalive (for the current chat).
322 // active profile. (Keepalive stays dormant until the first one — see `armed`.)333 eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
323 const activationEvents = [334
324 event_types.MESSAGE_SENT,335 // These reset the active profile's idle clock (so generation time isn't counted as idle) but
336 // never arm keepalive on their own — they also fire for background/quiet generations and while
337 // a chat is loading. They only re-arm the timer if a sent message already armed it.
338 const activityEvents = [
325 event_types.MESSAGE_RECEIVED,339 event_types.MESSAGE_RECEIVED,
326 event_types.GENERATION_STARTED,340 event_types.GENERATION_STARTED,
327 event_types.GENERATION_ENDED,341 event_types.GENERATION_ENDED,
328 ];342 ];
329 for (const event of activationEvents) {343 for (const event of activityEvents) {
330 eventSource.on(event, onChatActivity);344 eventSource.on(event, markActivity);
331 }345 }
332346
333 // Switching chats re-targets the per-profile idle clock but does NOT arm keepalive on its347 // Opening/switching a chat disarms keepalive — it must be re-armed by a new sent message.
334 // own — opening/loading a chat is not proof that anything is cached server-side.348 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
335 eventSource.on(event_types.CHAT_CHANGED, markActivity);
336349
337 // When the active profile changes, re-target the idle timer at the new profile using350 // When the active profile changes, re-target the idle timer at the new profile using
338 // ITS own last-activity time (do NOT mark activity — switching is not using it).351 // ITS own last-activity time (do NOT mark activity — switching is not using it).
public/scripts/extensions/keepalive/settings.html+1 -1
@@ -16,7 +16,7 @@
16 <label for="keepalive_message" data-i18n="ext_keepalive_message">Keepalive message</label>16 <label for="keepalive_message" data-i18n="ext_keepalive_message">Keepalive message</label>
17 <input id="keepalive_message" class="text_pole" type="text" />17 <input id="keepalive_message" class="text_pole" type="text" />
1818
19 <small data-i18n="ext_keepalive_help">After this many seconds without activity on the active connection profile, the current chat plus this message (sent as a hidden user message) is submitted to that profile to keep its server-side prompt cache warm. The completion is capped to a single token and is never saved to the chat. Sending is briefly locked while it runs. Only the profile you're currently using is pinged; each profile has its own idle timer. Keepalive stays off until you've sent at least one message since opening the tab (a freshly loaded chat isn't proof anything is still cached).</small>19 <small data-i18n="ext_keepalive_help">After this many seconds without activity on the active connection profile, the current chat plus this message (sent as a hidden user message) is submitted to that profile to keep its server-side prompt cache warm. The completion is capped to a single token and is never saved to the chat. Sending is briefly locked while it runs. Only the profile you're currently using is pinged; each profile has its own idle timer. Keepalive stays off until you send a new message in the current chat (opening, switching to, or reloading a chat isn't proof its prompt is still cached), and it also skips a ping if its timer fired far later than scheduled (e.g. after the machine slept).</small>
2020
21 <hr>21 <hr>
2222