Isolate text keepalive from image generation

ad7958ed1cbe509a6ebeac4e41fb206b52a46942

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

4 files changed, +53 -9Ignore whitespace
public/scripts/events.js+2 -0
@@ -79,6 +79,8 @@ export const event_types = {
79 ONLINE_STATUS_CHANGED: 'online_status_changed',79 ONLINE_STATUS_CHANGED: 'online_status_changed',
80 IMAGE_SWIPED: 'image_swiped',80 IMAGE_SWIPED: 'image_swiped',
81 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',81 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
82 CONNECTION_PROFILE_TEMPORARY_STARTED: 'connection_profile_temporary_started',
83 CONNECTION_PROFILE_TEMPORARY_ENDED: 'connection_profile_temporary_ended',
82 CONNECTION_PROFILE_CREATED: 'connection_profile_created',84 CONNECTION_PROFILE_CREATED: 'connection_profile_created',
83 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',85 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
84 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',86 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
public/scripts/extensions/keepalive/index.js+44 -7
@@ -54,6 +54,9 @@ let generationRefreshTimer = null;
54let retryTimer = null;54let retryTimer = null;
55let preparingJob = false;55let preparingJob = false;
56let lifecycleVersion = 0;56let lifecycleVersion = 0;
57let foregroundGenerationActive = false;
58let temporaryConnectionDepth = 0;
59let refreshAfterTemporaryConnection = false;
57// Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new60// Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new
58// message or starts another foreground generation in the open chat. Merely loading a chat does not61// message or starts another foreground generation in the open chat. Merely loading a chat does not
59// prove that its prompt is cached server-side. Reset on page reload and whenever the chat changes.62// prove that its prompt is cached server-side. Reset on page reload and whenever the chat changes.
@@ -145,7 +148,7 @@ async function stopBackendJob() {
145}148}
146149
147async function sendBackendHeartbeat(activity = false) {150async function sendBackendHeartbeat(activity = false) {
148 if (!armed || !isKeepaliveEnabledForActive()) {151 if (temporaryConnectionDepth > 0 || !armed || !isKeepaliveEnabledForActive()) {
149 return;152 return;
150 }153 }
151 // Registration is normally created as soon as a response finishes. If that one-shot event154 // Registration is normally created as soon as a response finishes. If that one-shot event
@@ -182,7 +185,7 @@ async function sendBackendHeartbeat(activity = false) {
182}185}
183186
184function markActivity() {187function markActivity() {
185 if (preparingJob) {188 if (preparingJob || temporaryConnectionDepth > 0) {
186 return;189 return;
187 }190 }
188 lastActivity[getActiveProfileKey()] = Date.now();191 lastActivity[getActiveProfileKey()] = Date.now();
@@ -296,6 +299,10 @@ async function prepareKeepaliveRequest() {
296}299}
297300
298async function registerBackendJob() {301async function registerBackendJob() {
302 if (temporaryConnectionDepth > 0) {
303 refreshAfterTemporaryConnection = true;
304 return;
305 }
299 if (!armed || !isKeepaliveEnabledForActive()) {306 if (!armed || !isKeepaliveEnabledForActive()) {
300 await stopBackendJob();307 await stopBackendJob();
301 return;308 return;
@@ -500,23 +507,34 @@ function setupListeners() {
500}507}
501508
502function onGenerationStarted(type, _options, dryRun) {509function onGenerationStarted(type, _options, dryRun) {
503 if (dryRun) {510 // Quiet/background generations (image prompts, summaries, helpers, etc.) use a different model
511 // or context and do not refresh the foreground text-chat cache.
512 if (dryRun || type === 'quiet') {
504 return;513 return;
505 }514 }
506 // Regenerates, swipes, and continues send the current prompt to the provider without emitting515 // Regenerates, swipes, and continues send the current prompt to the provider without emitting
507 // MESSAGE_SENT. They are just as valid proof of a warm prompt as a newly typed message.516 // MESSAGE_SENT. They are just as valid proof of a warm prompt as a newly typed message.
508 if (type !== 'quiet') {517 foregroundGenerationActive = true;
509 armed = true;518 armed = true;
510 }
511 markActivity();519 markActivity();
512}520}
513521
514function onGenerationEnded() {522function onGenerationEnded() {
523 if (!foregroundGenerationActive) {
524 return;
525 }
526 foregroundGenerationActive = false;
515 markActivity();527 markActivity();
516 scheduleGenerationJobRefresh();528 scheduleGenerationJobRefresh();
517}529}
518530
519function onMessageReceived() {531function onMessageReceived(_messageId, type) {
532 // Image-generation extensions add their results as chat messages. Those messages neither used
533 // nor refreshed the foreground text cache, so they must not move its keepalive deadline.
534 if (type === 'extension') {
535 return;
536 }
537 foregroundGenerationActive = false;
520 markActivity();538 markActivity();
521 scheduleGenerationJobRefresh();539 scheduleGenerationJobRefresh();
522}540}
@@ -529,6 +547,9 @@ function onChatContentChanged() {
529}547}
530548
531async function onConnectionChanged() {549async function onConnectionChanged() {
550 if (temporaryConnectionDepth > 0) {
551 return;
552 }
532 // Remove the previous connection's request before assembling one with the new settings.553 // Remove the previous connection's request before assembling one with the new settings.
533 await stopBackendJob();554 await stopBackendJob();
534 if (armed && isKeepaliveEnabledForActive()) {555 if (armed && isKeepaliveEnabledForActive()) {
@@ -536,6 +557,18 @@ async function onConnectionChanged() {
536 }557 }
537}558}
538559
560function onTemporaryConnectionStarted() {
561 temporaryConnectionDepth++;
562}
563
564function onTemporaryConnectionEnded() {
565 temporaryConnectionDepth = Math.max(0, temporaryConnectionDepth - 1);
566 if (temporaryConnectionDepth === 0 && refreshAfterTemporaryConnection) {
567 refreshAfterTemporaryConnection = false;
568 queueJobRefresh();
569 }
570}
571
539async function init() {572async function init() {
540 const settingsHtml = await renderExtensionTemplateAsync(MODULE, 'settings');573 const settingsHtml = await renderExtensionTemplateAsync(MODULE, 'settings');
541 $('#extensions_settings2').append(settingsHtml);574 $('#extensions_settings2').append(settingsHtml);
@@ -554,6 +587,7 @@ async function init() {
554 eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);587 eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
555 eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);588 eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
556 eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);589 eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
590 eventSource.on(event_types.GENERATION_STOPPED, () => { foregroundGenerationActive = false; });
557 eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged);591 eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged);
558 eventSource.on(event_types.MESSAGE_DELETED, onChatContentChanged);592 eventSource.on(event_types.MESSAGE_DELETED, onChatContentChanged);
559 eventSource.on(event_types.MESSAGE_UPDATED, onChatContentChanged);593 eventSource.on(event_types.MESSAGE_UPDATED, onChatContentChanged);
@@ -563,6 +597,9 @@ async function init() {
563 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);597 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
564598
565 // When the active profile changes, replace the backend job without marking it as activity.599 // When the active profile changes, replace the backend job without marking it as activity.
600 // Temporary profile switches used by background image-prompt generation are explicitly ignored.
601 eventSource.on(event_types.CONNECTION_PROFILE_TEMPORARY_STARTED, onTemporaryConnectionStarted);
602 eventSource.on(event_types.CONNECTION_PROFILE_TEMPORARY_ENDED, onTemporaryConnectionEnded);
566 eventSource.on(event_types.CONNECTION_PROFILE_LOADED, onConnectionChanged);603 eventSource.on(event_types.CONNECTION_PROFILE_LOADED, onConnectionChanged);
567 eventSource.on(event_types.MAIN_API_CHANGED, onConnectionChanged);604 eventSource.on(event_types.MAIN_API_CHANGED, onConnectionChanged);
568 eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, onConnectionChanged);605 eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, onConnectionChanged);
public/scripts/extensions/keepalive/manifest.json+1 -1
@@ -6,7 +6,7 @@
6 "js": "index.js",6 "js": "index.js",
7 "css": "",7 "css": "",
8 "author": "SillyTavern",8 "author": "SillyTavern",
9 "version": "1.1.3",9 "version": "1.1.4",
10 "homePage": "https://github.com/SillyTavern/SillyTavern",10 "homePage": "https://github.com/SillyTavern/SillyTavern",
11 "hooks": {11 "hooks": {
12 "activate": "init"12 "activate": "init"
public/scripts/extensions/stable-diffusion/index.js+6 -1
@@ -4714,14 +4714,19 @@ async function withConnectionProfile(targetProfileId, callback) {
4714 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false });4714 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false });
4715 };4715 };
47164716
4717 await switchToProfile(targetProfileId);4717 // Background image-prompt work must not disturb foreground features that follow the user's
4718 // active connection profile (notably text prompt-cache keepalives).
4719 await eventSource.emit(event_types.CONNECTION_PROFILE_TEMPORARY_STARTED);
4718 try {4720 try {
4721 await switchToProfile(targetProfileId);
4719 return await callback();4722 return await callback();
4720 } finally {4723 } finally {
4721 try {4724 try {
4722 await switchToProfile(currentProfileId);4725 await switchToProfile(currentProfileId);
4723 } catch (err) {4726 } catch (err) {
4724 console.error('SD: failed to restore the previous connection profile after image-prompt generation', err);4727 console.error('SD: failed to restore the previous connection profile after image-prompt generation', err);
4728 } finally {
4729 await eventSource.emit(event_types.CONNECTION_PROFILE_TEMPORARY_ENDED);
4725 }4730 }
4726 }4731 }
4727}4732}