-
-
Notifications
You must be signed in to change notification settings - Fork 9.2k
📴 fix: Persist Cancelled Compactions With Their Identity #16436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
6f93ddb
bd24612
cc3851f
dd20c8a
5a6b8b7
92d0b0f
a83adc8
851017d
4bd15d9
ed11f82
ea266b5
bb9ebea
e223e91
3490456
8d53e95
e4c194b
c77c793
b724f5e
fb85ea6
2702225
2485ce8
86e701b
418342c
929997a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,10 @@ const { | |
| resolvePersistableCodeEnvironmentDecision, | ||
| getFailedTurnTraceFields, | ||
| resolveFailedTurnContent, | ||
| settleExistingRowsBeforeErrorTurn, | ||
| resolveDisconnectSnapshotMode, | ||
| resolveReconciledSnapshotEnvelope, | ||
| markAbortedCompactionContent, | ||
| } = require('@librechat/api'); | ||
| const { disposeClient } = require('~/server/cleanup'); | ||
| const { | ||
|
|
@@ -426,23 +430,6 @@ async function saveErrorTurn( | |
| } | ||
|
|
||
| const userId = req.user.id; | ||
| const existing = await getMessages( | ||
| { user: userId, messageId: errorMessageId, conversationId }, | ||
| '_id', | ||
| ); | ||
| if (existing.length > 0) { | ||
| return; | ||
| } | ||
| if (liveResponseMessageId != null && liveResponseMessageId !== errorMessageId) { | ||
| const partial = await getMessages( | ||
| { user: userId, messageId: liveResponseMessageId, conversationId }, | ||
| '_id', | ||
| ); | ||
| if (partial.length > 0) { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const reqCtx = { | ||
| userId, | ||
| isTemporary: | ||
|
|
@@ -453,6 +440,29 @@ async function saveErrorTurn( | |
| req?._agentEventBindingRetention?.expiredAt ?? req?.resolvedConversation?.expiredAt, | ||
| interfaceConfig: req?.config?.interfaceConfig, | ||
| }; | ||
| /** The existing-row settlement (which row a failed turn settles, and | ||
| * whether its error row may be written at all) lives in @librechat/api; | ||
| * this supplies the caller's reads and write. */ | ||
| const settlement = await settleExistingRowsBeforeErrorTurn(req.body, { | ||
| userId, | ||
| conversationId, | ||
| errorMessageId, | ||
| liveResponseMessageId, | ||
| getMessages, | ||
| saveFinalizedTurn: (message) => | ||
| saveMessage(reqCtx, message, { | ||
| context: 'api/server/controllers/agents/request.js - finalize failed compaction turn', | ||
| }), | ||
| }); | ||
| if (settlement.covered) { | ||
| return; | ||
| } | ||
| /** The anchor-shaped collision redirects the error row to the failed | ||
| * run's own response id, so it can never overwrite the anchor. */ | ||
| if (settlement.errorRowMessageId != null) { | ||
| errorMessageId = settlement.errorRowMessageId; | ||
| } | ||
|
|
||
| const context = 'api/server/controllers/agents/request.js - failed turn'; | ||
| const endpoint = endpointOption?.endpoint; | ||
| const model = getAgentResponseModel(req, endpointOption); | ||
|
|
@@ -1663,6 +1673,9 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit | |
| * no user message of its own, the response parented onto an | ||
| * existing message. A reconnecting client rebuilds it that way. */ | ||
| ...((isRegenerate || isCompaction) && { isRegenerate: true }), | ||
| /** The job record is where the abort paths learn the turn was a | ||
| * compaction: they run after the request that created the job. */ | ||
| ...(isCompaction && { compact: true }), | ||
| ...(scheduleId | ||
| ? { | ||
| scheduleId, | ||
|
|
@@ -1849,13 +1862,10 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit | |
| * overwrite this with the complete response using the same messageId pattern. | ||
| */ | ||
| job.emitter.on('allSubscribersLeft', async (aggregatedContent) => { | ||
| if (partialResponseSaved || !aggregatedContent || aggregatedContent.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const persistableContent = filterPersistableAbortContent(aggregatedContent); | ||
| if (persistableContent.length === 0) { | ||
| logger.debug('[ResumableAgentController] No persistable content to save partial response'); | ||
| /** Empty content is rejected only after the snapshot mode is known: a | ||
| * reconciled compaction synthesizes its terminal outcome from nothing, | ||
| * while a live run has nothing persistable. */ | ||
| if (partialResponseSaved || !aggregatedContent) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -1868,6 +1878,47 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit | |
| return; | ||
| } | ||
|
|
||
| /** How this snapshot may persist is decided in @librechat/api: live | ||
| * runs keep the marker-only shape, a failed terminal write (settled | ||
| * for a reconciliation frame) promotes the snapshot to the turn's | ||
| * terminal row, and a durably settled compaction withholds it. */ | ||
| const snapshotMode = await resolveDisconnectSnapshotMode( | ||
| isCompaction, | ||
| jobRecord, | ||
| jobCreatedAt, | ||
| { | ||
| anchorExists: async () => | ||
| ( | ||
| await getMessages( | ||
| { | ||
| user: userId, | ||
| messageId: resumeState.userMessage.messageId, | ||
| conversationId, | ||
| }, | ||
| '_id', | ||
| ) | ||
| ).length > 0, | ||
| }, | ||
| ); | ||
| if (snapshotMode === 'skip') { | ||
| logger.debug( | ||
| '[ResumableAgentController] Skipping compaction partial save for a settled job', | ||
| ); | ||
| return; | ||
| } | ||
| if (snapshotMode !== 'terminal' && aggregatedContent.length === 0) { | ||
| return; | ||
| } | ||
| const persistableContent = markAbortedCompactionContent( | ||
| filterPersistableAbortContent(aggregatedContent), | ||
| isCompaction, | ||
| { synthesizeFailure: snapshotMode === 'terminal' }, | ||
|
Comment on lines
+1912
to
+1915
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Stop or the error path reconciles after its primary write fails before any content was emitted, closing the remaining subscriber invokes this handler with AGENTS.md reference: AGENTS.md:L49-L52 Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 929997a. Empty content is rejected only for live snapshots; a promoted terminal snapshot synthesizes the typed failure from nothing, with a controller regression. |
||
| ); | ||
| if (persistableContent.length === 0) { | ||
| logger.debug('[ResumableAgentController] No persistable content to save partial response'); | ||
| return; | ||
| } | ||
|
|
||
| partialResponseSaved = true; | ||
| const responseConversationId = resumeState.conversationId || conversationId; | ||
| /** The run publishes its calibration and fading tiers onto the job; a | ||
|
|
@@ -1885,8 +1936,12 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit | |
| parentMessageId: resumeState.userMessage.messageId, | ||
| sender: client?.sender ?? 'AI', | ||
| content: persistableContent, | ||
| unfinished: true, | ||
| error: false, | ||
| /** A snapshot promoted to the terminal row settles with the | ||
| * envelope its reconciled claim's status dictates; a live-run | ||
| * snapshot keeps the live shape. */ | ||
| ...(snapshotMode === 'terminal' | ||
| ? resolveReconciledSnapshotEnvelope(jobRecord?.status) | ||
| : { unfinished: true, error: false }), | ||
| isCreatedByUser: false, | ||
| user: userId, | ||
| endpoint: endpointOption.endpoint, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the first disconnect occurs while a compaction is live, this handler saves an
unfinished: truesnapshot and setspartialResponseSaved; if the completed response write later fails and the terminal claim settles to a reconciliation frame, there may be no further disconnect callback, and any callback that does occur returns here beforeresolveDisconnectSnapshotModecan promote the row. Unlike the previously reviewed delayed-first-callback ordering, the fresh evidence is this successful-earlier-callback ordering: a completed job can remain permanently stored as an unfinished live response after reload. Settle the already-saved snapshot from the terminal persistence-failure path, or allow reconciliation to reprocess it.AGENTS.md reference: AGENTS.md:L45-L51
Useful? React with 👍 / 👎.