Isolate text keepalive from image generation
| @@ -79,6 +79,8 @@ export const event_types = { | ||
| 79 | 79 | ONLINE_STATUS_CHANGED: 'online_status_changed', |
| 80 | 80 | IMAGE_SWIPED: 'image_swiped', |
| 81 | 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 | 84 | CONNECTION_PROFILE_CREATED: 'connection_profile_created', |
| 83 | 85 | CONNECTION_PROFILE_DELETED: 'connection_profile_deleted', |
| 84 | 86 | CONNECTION_PROFILE_UPDATED: 'connection_profile_updated', |
| @@ -54,6 +54,9 @@ let generationRefreshTimer = null; | ||
| 54 | 54 | let retryTimer = null; |
| 55 | 55 | let preparingJob = false; |
| 56 | 56 | let lifecycleVersion = 0; |
| 57 | +let foregroundGenerationActive = false; | |
| 58 | +let temporaryConnectionDepth = 0; | |
| 59 | +let refreshAfterTemporaryConnection = false; | |
| 57 | 60 | // Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new |
| 58 | 61 | // message or starts another foreground generation in the open chat. Merely loading a chat does not |
| 59 | 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 | } |
| 146 | 149 | |
| 147 | 150 | async function sendBackendHeartbeat(activity = false) { |
| 148 | 151 | if (temporaryConnectionDepth > 0 || !armed || !isKeepaliveEnabledForActive()) { |
| 149 | 152 | return; |
| 150 | 153 | } |
| 151 | 154 | // 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 | } |
| 183 | 186 | |
| 184 | 187 | function markActivity() { |
| 185 | 188 | if (preparingJob || temporaryConnectionDepth > 0) { |
| 186 | 189 | return; |
| 187 | 190 | } |
| 188 | 191 | lastActivity[getActiveProfileKey()] = Date.now(); |
| @@ -296,6 +299,10 @@ async function prepareKeepaliveRequest() { | ||
| 296 | 299 | } |
| 297 | 300 | |
| 298 | 301 | async function registerBackendJob() { |
| 302 | + if (temporaryConnectionDepth > 0) { | |
| 303 | + refreshAfterTemporaryConnection = true; | |
| 304 | + return; | |
| 305 | + } | |
| 299 | 306 | if (!armed || !isKeepaliveEnabledForActive()) { |
| 300 | 307 | await stopBackendJob(); |
| 301 | 308 | return; |
| @@ -500,23 +507,34 @@ function setupListeners() { | ||
| 500 | 507 | } |
| 501 | 508 | |
| 502 | 509 | function 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 | 513 | return; |
| 505 | 514 | } |
| 506 | 515 | // Regenerates, swipes, and continues send the current prompt to the provider without emitting |
| 507 | 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 | 518 | armed = true; |
| 510 | - } | |
| 511 | 519 | markActivity(); |
| 512 | 520 | } |
| 513 | 521 | |
| 514 | 522 | function onGenerationEnded() { |
| 523 | + if (!foregroundGenerationActive) { | |
| 524 | + return; | |
| 525 | + } | |
| 526 | + foregroundGenerationActive = false; | |
| 515 | 527 | markActivity(); |
| 516 | 528 | scheduleGenerationJobRefresh(); |
| 517 | 529 | } |
| 518 | 530 | |
| 519 | 531 | function 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 | 538 | markActivity(); |
| 521 | 539 | scheduleGenerationJobRefresh(); |
| 522 | 540 | } |
| @@ -529,6 +547,9 @@ function onChatContentChanged() { | ||
| 529 | 547 | } |
| 530 | 548 | |
| 531 | 549 | async function onConnectionChanged() { |
| 550 | + if (temporaryConnectionDepth > 0) { | |
| 551 | + return; | |
| 552 | + } | |
| 532 | 553 | // Remove the previous connection's request before assembling one with the new settings. |
| 533 | 554 | await stopBackendJob(); |
| 534 | 555 | if (armed && isKeepaliveEnabledForActive()) { |
| @@ -536,6 +557,18 @@ async function onConnectionChanged() { | ||
| 536 | 557 | } |
| 537 | 558 | } |
| 538 | 559 | |
| 560 | +function onTemporaryConnectionStarted() { | |
| 561 | + temporaryConnectionDepth++; | |
| 562 | +} | |
| 563 | + | |
| 564 | +function onTemporaryConnectionEnded() { | |
| 565 | + temporaryConnectionDepth = Math.max(0, temporaryConnectionDepth - 1); | |
| 566 | + if (temporaryConnectionDepth === 0 && refreshAfterTemporaryConnection) { | |
| 567 | + refreshAfterTemporaryConnection = false; | |
| 568 | + queueJobRefresh(); | |
| 569 | + } | |
| 570 | +} | |
| 571 | + | |
| 539 | 572 | async function init() { |
| 540 | 573 | const settingsHtml = await renderExtensionTemplateAsync(MODULE, 'settings'); |
| 541 | 574 | $('#extensions_settings2').append(settingsHtml); |
| @@ -554,6 +587,7 @@ async function init() { | ||
| 554 | 587 | eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); |
| 555 | 588 | eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted); |
| 556 | 589 | eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded); |
| 590 | + eventSource.on(event_types.GENERATION_STOPPED, () => { foregroundGenerationActive = false; }); | |
| 557 | 591 | eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged); |
| 558 | 592 | eventSource.on(event_types.MESSAGE_DELETED, onChatContentChanged); |
| 559 | 593 | eventSource.on(event_types.MESSAGE_UPDATED, onChatContentChanged); |
| @@ -563,6 +597,9 @@ async function init() { | ||
| 563 | 597 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 564 | 598 | |
| 565 | 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 | 603 | eventSource.on(event_types.CONNECTION_PROFILE_LOADED, onConnectionChanged); |
| 567 | 604 | eventSource.on(event_types.MAIN_API_CHANGED, onConnectionChanged); |
| 568 | 605 | eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, onConnectionChanged); |
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | "js": "index.js", |
| 7 | 7 | "css": "", |
| 8 | 8 | "author": "SillyTavern", |
| 9 | 9 | "version": "1.1.34", |
| 10 | 10 | "homePage": "https://github.com/SillyTavern/SillyTavern", |
| 11 | 11 | "hooks": { |
| 12 | 12 | "activate": "init" |
| @@ -4714,14 +4714,19 @@ async function withConnectionProfile(targetProfileId, callback) { | ||
| 4714 | 4714 | await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false }); |
| 4715 | 4715 | }; |
| 4716 | 4716 | |
| 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 | 4720 | try { |
| 4721 | + await switchToProfile(targetProfileId); | |
| 4719 | 4722 | return await callback(); |
| 4720 | 4723 | } finally { |
| 4721 | 4724 | try { |
| 4722 | 4725 | await switchToProfile(currentProfileId); |
| 4723 | 4726 | } catch (err) { |
| 4724 | 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 | } |