mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🩹 fix: Restore the @librechat/api Build and Remove Legacy Code (#14808)
* 🧹 chore: Remove Dead Legacy Agent Controller `_LegacyAgentController` has been unreachable since the resumable path became the only route: it is unreferenced, unexported, and untested. It had also drifted out of compilability against the live file — line 2009 called `attachConversationCreatedAt(req, { userId, conversationId, isNewConvo })` against the 3-argument signature declared at line 97, which would await `undefined` and then throw dereferencing `resolved.createdAt`. Keeping it was not free. It carried a third independent copy of the response message-id wiring (`getReqData`, `onStart`, four `updateMetadata` calls), so every change to how a generation identifies its response row had a dead third site to keep in step, and no test to say whether it had been kept in step. Removing the block leaves `createCloseHandler` and the `sendEvent`, `clientRegistry`, `requestDataMap` and `handleAbortError` imports with no remaining callers, so those go too. `AgentController` was a three-line passthrough to `ResumableAgentController`; the real controller is now exported directly, which also matches the `[ResumableAgentController]` prefix every log line in the file already uses. `server/routes/agents/chat.js` binds the export to its own local name and passes the same five arguments, so the route is unchanged. No behavior change: 379 lines removed, 2 added. * 🩹 fix: Remove Duplicated Anchor Block Breaking the `@librechat/api` Build `dev` does not build. `packages/api/src/agents/activityPhases/runtime.ts` carries two byte-identical 98-line copies of the same block (former lines 516-613 and 614-711), so rolldown fails to parse it: [PARSE_ERROR] Identifier `AnchorFields` has already been declared The duplicated block is the anchor-construction work from #14805: `AnchorFields`, `laterDefinedIndex`, `foldedAgentIds`, `boundedAnchor` and `mergeAnchors`. #14807 was squashed from a branch that predated #14805 and re-included that commit, so both copies landed. Only the `type` produced an error — the four function declarations simply redeclare. This removes the first copy. The two blocks were verified byte-identical before the cut, and the resulting file has no duplicate top-level declarations, is missing nothing that #14805 introduced, and retains everything new to #14807 (`ResolvedPosition`, `resolvePosition`). Verified: `tsdown` builds, `tsc --noEmit` clean, `config/circular-deps.mjs` green across all five graphs (it was reporting `✗ @librechat/api` purely because the build it shells out to was failing), and the 68 tests in `activityPhases/runtime.spec.ts` pass. Carried here rather than in a separate PR because this PR's checks cannot go green until it lands: the failed `packages/api` build cascades into e2e, MCP list_changed, bombadil and the Docker image jobs.
This commit is contained in:
parent
5bd745783c
commit
abc669ab58
2 changed files with 2 additions and 475 deletions
|
|
@ -8,7 +8,6 @@ const {
|
|||
isEphemeralAgentId,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
sendEvent,
|
||||
toPendingSteer,
|
||||
getViolationInfo,
|
||||
buildMessageFiles,
|
||||
|
|
@ -27,12 +26,11 @@ const {
|
|||
buildRecoveredSteerPayload,
|
||||
deleteAgentCheckpoint,
|
||||
} = require('@librechat/api');
|
||||
const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup');
|
||||
const { disposeClient } = require('~/server/cleanup');
|
||||
const {
|
||||
getMCPRequestContext,
|
||||
cleanupMCPRequestContextForReq,
|
||||
} = require('~/server/services/MCPRequestContext');
|
||||
const { handleAbortError } = require('~/server/middleware');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { saveMessage, getMessages, getConvo } = require('~/models');
|
||||
const {
|
||||
|
|
@ -63,24 +61,6 @@ function getResourceRecoveryFailure(error) {
|
|||
};
|
||||
}
|
||||
|
||||
function createCloseHandler(abortController) {
|
||||
return function (manual) {
|
||||
if (!manual) {
|
||||
logger.debug('[AgentController] Request closed');
|
||||
}
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[AgentController] Request aborted on close');
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConversationCreatedAt({ userId, conversationId, isNewConvo }) {
|
||||
return resolveConversationAnchor({
|
||||
isNewConversation: isNewConvo,
|
||||
|
|
@ -1953,359 +1933,4 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Agent Controller - Routes to ResumableAgentController for all requests.
|
||||
* The legacy non-resumable path is kept below but no longer used by default.
|
||||
*/
|
||||
const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
return ResumableAgentController(req, res, next, initializeClient, addTitle);
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy Non-resumable Agent Controller - Uses GenerationJobManager for abort handling.
|
||||
* Response is streamed directly to client via res, but abort state is managed centrally.
|
||||
* @deprecated Use ResumableAgentController instead
|
||||
*/
|
||||
const _LegacyAgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
const {
|
||||
text,
|
||||
isRegenerate,
|
||||
endpointOption,
|
||||
conversationId: reqConversationId,
|
||||
isContinued = false,
|
||||
editedContent = null,
|
||||
parentMessageId = null,
|
||||
overrideParentMessageId = null,
|
||||
responseMessageId: editedResponseMessageId = null,
|
||||
} = req.body;
|
||||
|
||||
// Generate conversationId upfront if not provided - streamId === conversationId always
|
||||
// Treat "new" as a placeholder that needs a real UUID (frontend may send "new" for new convos)
|
||||
const isNewConvo = !reqConversationId || reqConversationId === 'new';
|
||||
const conversationId = isNewConvo ? crypto.randomUUID() : reqConversationId;
|
||||
const streamId = conversationId;
|
||||
|
||||
let userMessage;
|
||||
let userMessageId;
|
||||
let responseMessageId;
|
||||
let client = null;
|
||||
let jobCreatedAt;
|
||||
let cleanupHandlers = [];
|
||||
|
||||
// Match the same logic used for conversationId generation above
|
||||
const userId = req.user.id;
|
||||
|
||||
if (
|
||||
await isUnpersistedPreliminaryParent({
|
||||
userId,
|
||||
conversationId: reqConversationId,
|
||||
parentMessageId,
|
||||
getMessages,
|
||||
})
|
||||
) {
|
||||
return rejectPreliminaryParentMessageId(res);
|
||||
}
|
||||
|
||||
await attachConversationCreatedAt(req, { userId, conversationId, isNewConvo });
|
||||
|
||||
// Create handler to avoid capturing the entire parent scope
|
||||
let getReqData = (data = {}) => {
|
||||
for (let key in data) {
|
||||
if (key === 'userMessage') {
|
||||
userMessage = data[key];
|
||||
userMessageId = data[key].messageId;
|
||||
} else if (key === 'responseMessageId') {
|
||||
responseMessageId = data[key];
|
||||
} else if (key === 'promptTokens') {
|
||||
// Update job metadata with prompt tokens for abort handling
|
||||
GenerationJobManager.updateMetadata(streamId, { promptTokens: data[key] }, jobCreatedAt);
|
||||
} else if (key === 'sender') {
|
||||
GenerationJobManager.updateMetadata(streamId, { sender: data[key] }, jobCreatedAt);
|
||||
}
|
||||
// conversationId is pre-generated, no need to update from callback
|
||||
}
|
||||
};
|
||||
|
||||
// Create a function to handle final cleanup
|
||||
const performCleanup = async () => {
|
||||
logger.debug('[AgentController] Performing cleanup');
|
||||
if (Array.isArray(cleanupHandlers)) {
|
||||
for (const handler of cleanupHandlers) {
|
||||
try {
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[AgentController] Error in cleanup handler', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the job in GenerationJobManager
|
||||
if (jobCreatedAt != null) {
|
||||
logger.debug('[AgentController] Completing job in GenerationJobManager');
|
||||
await GenerationJobManager.completeJob(streamId, undefined, jobCreatedAt);
|
||||
}
|
||||
|
||||
// Dispose client properly
|
||||
if (client) {
|
||||
disposeClient(client);
|
||||
}
|
||||
|
||||
// Clear all references
|
||||
client = null;
|
||||
getReqData = null;
|
||||
userMessage = null;
|
||||
cleanupHandlers = null;
|
||||
|
||||
// Clear request data map
|
||||
if (requestDataMap.has(req)) {
|
||||
requestDataMap.delete(req);
|
||||
}
|
||||
logger.debug('[AgentController] Cleanup completed');
|
||||
};
|
||||
|
||||
try {
|
||||
let prelimAbortController = new AbortController();
|
||||
const prelimCloseHandler = createCloseHandler(prelimAbortController);
|
||||
res.on('close', prelimCloseHandler);
|
||||
const removePrelimHandler = (manual) => {
|
||||
try {
|
||||
prelimCloseHandler(manual);
|
||||
res.removeListener('close', prelimCloseHandler);
|
||||
} catch (e) {
|
||||
logger.error('[AgentController] Error removing close listener', e);
|
||||
}
|
||||
};
|
||||
cleanupHandlers.push(removePrelimHandler);
|
||||
|
||||
/** @type {{ client: TAgentClient; userMCPAuthMap?: Record<string, Record<string, string>> }} */
|
||||
const result = await initializeClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
signal: prelimAbortController.signal,
|
||||
});
|
||||
|
||||
if (prelimAbortController.signal?.aborted) {
|
||||
prelimAbortController = null;
|
||||
throw new Error('Request was aborted before initialization could complete');
|
||||
} else {
|
||||
prelimAbortController = null;
|
||||
removePrelimHandler(true);
|
||||
cleanupHandlers.pop();
|
||||
}
|
||||
client = result.client;
|
||||
|
||||
// Register client with finalization registry if available
|
||||
if (clientRegistry) {
|
||||
clientRegistry.register(client, { userId }, client);
|
||||
}
|
||||
|
||||
// Store request data in WeakMap keyed by req object
|
||||
requestDataMap.set(req, { client });
|
||||
|
||||
// Create job in GenerationJobManager for abort handling
|
||||
// streamId === conversationId (pre-generated above)
|
||||
const job = await GenerationJobManager.createJob(streamId, userId, conversationId);
|
||||
jobCreatedAt = job.createdAt;
|
||||
client.jobCreatedAt = jobCreatedAt;
|
||||
client.checkpointNamespace = job.metadata?.checkpointNamespace ?? '';
|
||||
|
||||
// Store endpoint metadata for abort handling
|
||||
GenerationJobManager.updateMetadata(
|
||||
streamId,
|
||||
{
|
||||
endpoint: endpointOption.endpoint,
|
||||
iconURL: getEndpointIconURL(req, endpointOption),
|
||||
model: getAgentResponseModel(req, endpointOption),
|
||||
sender: client?.sender,
|
||||
},
|
||||
jobCreatedAt,
|
||||
);
|
||||
|
||||
// Store content parts reference for abort
|
||||
if (client?.contentParts) {
|
||||
GenerationJobManager.setContentParts(streamId, client.contentParts, jobCreatedAt);
|
||||
}
|
||||
|
||||
const closeHandler = createCloseHandler(job.abortController);
|
||||
res.on('close', closeHandler);
|
||||
cleanupHandlers.push(() => {
|
||||
try {
|
||||
res.removeListener('close', closeHandler);
|
||||
} catch (e) {
|
||||
logger.error('[AgentController] Error removing close listener', e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* onStart callback - stores user message and response ID for abort handling
|
||||
*/
|
||||
const onStart = (userMsg, respMsgId, _isNewConvo) => {
|
||||
sendEvent(res, { message: userMsg, created: true });
|
||||
userMessage = userMsg;
|
||||
userMessageId = userMsg.messageId;
|
||||
responseMessageId = respMsgId;
|
||||
|
||||
// Store metadata for abort handling (conversationId is pre-generated)
|
||||
GenerationJobManager.updateMetadata(
|
||||
streamId,
|
||||
{
|
||||
responseMessageId: respMsgId,
|
||||
userMessage: {
|
||||
messageId: userMsg.messageId,
|
||||
parentMessageId: userMsg.parentMessageId,
|
||||
conversationId,
|
||||
text: userMsg.text,
|
||||
quotes: userMsg.quotes,
|
||||
},
|
||||
},
|
||||
jobCreatedAt,
|
||||
);
|
||||
};
|
||||
|
||||
const messageOptions = {
|
||||
user: userId,
|
||||
onStart,
|
||||
getReqData,
|
||||
isContinued,
|
||||
isRegenerate,
|
||||
editedContent,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
abortController: job.abortController,
|
||||
overrideParentMessageId,
|
||||
isEdited: !!editedContent,
|
||||
userMCPAuthMap: result.userMCPAuthMap,
|
||||
responseMessageId: editedResponseMessageId,
|
||||
progressOptions: {
|
||||
res,
|
||||
},
|
||||
};
|
||||
|
||||
let response = await client.sendMessage(text, messageOptions);
|
||||
|
||||
// Extract what we need and immediately break reference
|
||||
const messageId = response.messageId;
|
||||
const endpoint = endpointOption.endpoint;
|
||||
response.endpoint = endpoint;
|
||||
|
||||
// Store database promise locally
|
||||
const databasePromise = response.databasePromise;
|
||||
delete response.databasePromise;
|
||||
|
||||
// Resolve database-related data
|
||||
const { conversation: convoData = {} } = await databasePromise;
|
||||
const conversation = { ...convoData };
|
||||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (req.body.files && Array.isArray(client.options.attachments)) {
|
||||
const files = buildMessageFiles(req.body.files, client.options.attachments);
|
||||
if (files.length > 0) {
|
||||
userMessage.files = files;
|
||||
}
|
||||
delete userMessage.image_urls;
|
||||
}
|
||||
|
||||
// Only send if not aborted
|
||||
if (!job.abortController.signal.aborted) {
|
||||
// Create a new response object with minimal copies
|
||||
const finalResponse = { ...response };
|
||||
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: sanitizeMessageForTransmit(userMessage),
|
||||
responseMessage: finalResponse,
|
||||
});
|
||||
res.end();
|
||||
|
||||
// Save the message if needed
|
||||
if (client.savedMessageIds && !client.savedMessageIds.has(messageId)) {
|
||||
await saveMessage(
|
||||
{
|
||||
userId: req?.user?.id,
|
||||
isTemporary: req?.body?.isTemporary,
|
||||
interfaceConfig: req?.config?.interfaceConfig,
|
||||
},
|
||||
{ ...finalResponse, user: userId },
|
||||
{ context: 'api/server/controllers/agents/request.js - response end' },
|
||||
);
|
||||
}
|
||||
}
|
||||
// Edge case: sendMessage completed but abort happened during sendCompletion
|
||||
// We need to ensure a final event is sent
|
||||
else if (!res.headersSent && !res.finished) {
|
||||
logger.debug(
|
||||
'[AgentController] Handling edge case: `sendMessage` completed but aborted during `sendCompletion`',
|
||||
);
|
||||
|
||||
const finalResponse = { ...response };
|
||||
finalResponse.error = true;
|
||||
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: sanitizeMessageForTransmit(userMessage),
|
||||
responseMessage: finalResponse,
|
||||
error: { message: 'Request was aborted during completion' },
|
||||
});
|
||||
res.end();
|
||||
}
|
||||
|
||||
// Save user message if needed
|
||||
if (!client.skipSaveUserMessage) {
|
||||
await saveMessage(
|
||||
{
|
||||
userId: req?.user?.id,
|
||||
isTemporary: req?.body?.isTemporary,
|
||||
interfaceConfig: req?.config?.interfaceConfig,
|
||||
},
|
||||
userMessage,
|
||||
{ context: "api/server/controllers/agents/request.js - don't skip saving user message" },
|
||||
);
|
||||
}
|
||||
|
||||
// Add title if needed - extract minimal data
|
||||
if (addTitle && parentMessageId === Constants.NO_PARENT && isNewConvo) {
|
||||
addTitle(req, {
|
||||
text,
|
||||
response: { ...response },
|
||||
client,
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('[AgentController] Title generation started');
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[AgentController] Error in title generation', err);
|
||||
})
|
||||
.finally(() => {
|
||||
logger.debug('[AgentController] Title generation completed');
|
||||
performCleanup();
|
||||
});
|
||||
} else {
|
||||
performCleanup();
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error without capturing much scope
|
||||
handleAbortError(res, req, error, {
|
||||
conversationId,
|
||||
sender: client?.sender,
|
||||
messageId: responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||
userMessageId,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
|
||||
})
|
||||
.finally(() => {
|
||||
performCleanup();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AgentController;
|
||||
module.exports = ResumableAgentController;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue