Isolate text keepalive from image generation
| @@ -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', |
| @@ -54,6 +54,9 @@ let generationRefreshTimer = null; | |||
| 54 | let retryTimer = null; | 54 | let retryTimer = null; |
| 55 | let preparingJob = false; | 55 | let preparingJob = false; |
| 56 | let lifecycleVersion = 0; | 56 | let lifecycleVersion = 0; |
| 57 | let foregroundGenerationActive = false; | ||
| 58 | let temporaryConnectionDepth = 0; | ||
| 59 | let refreshAfterTemporaryConnection = false; | ||
| 57 | // Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new | 60 | // 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 not | 61 | // 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 | } |
| 146 | 149 | ||
| 147 | async function sendBackendHeartbeat(activity = false) { | 150 | async 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 event | 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 | function markActivity() { | 187 | function 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 | } |
| 297 | 300 | ||
| 298 | async function registerBackendJob() { | 301 | async 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 | } |
| 501 | 508 | ||
| 502 | function onGenerationStarted(type, _options, dryRun) { | 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 | return; | 513 | return; |
| 505 | } | 514 | } |
| 506 | // Regenerates, swipes, and continues send the current prompt to the provider without emitting | 515 | // 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 | } |
| 513 | 521 | ||
| 514 | function onGenerationEnded() { | 522 | function onGenerationEnded() { |
| 523 | if (!foregroundGenerationActive) { | ||
| 524 | return; | ||
| 525 | } | ||
| 526 | foregroundGenerationActive = false; | ||
| 515 | markActivity(); | 527 | markActivity(); |
| 516 | scheduleGenerationJobRefresh(); | 528 | scheduleGenerationJobRefresh(); |
| 517 | } | 529 | } |
| 518 | 530 | ||
| 519 | function onMessageReceived() { | 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 | markActivity(); | 538 | markActivity(); |
| 521 | scheduleGenerationJobRefresh(); | 539 | scheduleGenerationJobRefresh(); |
| 522 | } | 540 | } |
| @@ -529,6 +547,9 @@ function onChatContentChanged() { | |||
| 529 | } | 547 | } |
| 530 | 548 | ||
| 531 | async function onConnectionChanged() { | 549 | async 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 | } |
| 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 | async function init() { | 572 | async 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); |
| 564 | 598 | ||
| 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); |
| @@ -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" |
| @@ -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 | }; |
| 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 | 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 | } |