mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-05 22:18:56 +00:00
* feat: wake parent agents on child completion * wip: harden child completion wakeup lifecycle * fix: close the completion-wakeup static failures Type the durable-claim store fixture, the continue-envelope test helper, and the terminal message's task metadata so the wakeup suites compile against the shapes they actually exercise. Replace `Array.prototype.at`, which the package target library does not provide. Capture the prepared child thread in a non-optional local before the provider callback closes over it, and narrow the trigger envelope itself on `mode === 'continue'` rather than a separately copied mode, so reading the continue target is sound. Lift the parent-message fallback out of a nested ternary into a named resolver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1cCMDrTWaRNkmtKjpWELZ * test: cover the active-predecessor admission fence The Redis job-creation call gained a thirteenth scalar argument, so the spec helper reconstructed the HSET pairs one slot early and rebuilt an invalid job hash; three creation tests failed on that alone. Give the fence itself direct coverage in both store adapters, which it had none of despite deciding whether an automatic continuation may replace a live parent turn. Each proves a running and a requires_action predecessor are refused with the state a controller needs for a finite 409, that an absent or settled predecessor is admitted, and that an ordinary user turn without the policy still replaces its predecessor. The Redis case also asserts a refused continuation leaves the parent's durable job and chunks untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1cCMDrTWaRNkmtKjpWELZ * fix: harden completion wakeup rollout and claims * fix: close completion wakeup race windows * test: keep the child store fixture exact * fix: close final subagent wakeup gaps * fix: preserve ambiguous completion claims * fix: release pre-admission wakeup claims * fix: stabilize subagent completion recovery --------- Co-authored-by: Claude <noreply@anthropic.com>
87 lines
3.4 KiB
JavaScript
87 lines
3.4 KiB
JavaScript
const {
|
|
cacheConfig,
|
|
ioredisClient,
|
|
isEnabled,
|
|
registerShutdownTask,
|
|
duplicateIoRedisClient,
|
|
createSubagentThreadTaskStore,
|
|
createSubagentCompletionWakeupHandler,
|
|
RedisSubagentTaskControlTransport,
|
|
} = require('@librechat/api');
|
|
const db = require('~/models');
|
|
const { enqueueAgentTrigger } = require('../../Agents/triggers');
|
|
|
|
/** Keep producers off for the first rollout so older trigger workers cannot
|
|
* permanently reject the new `continue` envelope. Enable only after every API
|
|
* replica runs a release that understands completion wakeups. */
|
|
const completionWakeupsEnabled = isEnabled(process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS);
|
|
|
|
/** Durable logical threads use normal LibreChat conversations/messages. Mongo
|
|
* fences continuation; optional Redis routing reaches the live owning process. */
|
|
const subagentThreadTaskStore = createSubagentThreadTaskStore(
|
|
{
|
|
acquireSubagentThreadLease: db.acquireSubagentThreadLease,
|
|
claimSubagentTaskResult: db.claimSubagentTaskResult,
|
|
releaseSubagentTaskResultClaim: db.releaseSubagentTaskResultClaim,
|
|
countActiveSubagentThreadLeases: db.countActiveSubagentThreadLeases,
|
|
deleteConvos: db.deleteConvos,
|
|
deleteMessages: db.deleteMessages,
|
|
getConvo: db.getConvo,
|
|
getMessages: db.getMessages,
|
|
listActiveSubagentThreadLeases: db.listActiveSubagentThreadLeases,
|
|
releaseSubagentThreadLease: db.releaseSubagentThreadLease,
|
|
reserveSubagentThread: db.reserveSubagentThread,
|
|
renewSubagentThreadLease: db.renewSubagentThreadLease,
|
|
saveConvo: db.saveConvo,
|
|
saveMessage: db.saveMessage,
|
|
},
|
|
{
|
|
isOwnerActive: db.isSubagentOwnerAdmissible,
|
|
fenceOwnerAdmission: db.fenceSubagentAdmission,
|
|
renewOwnerAdmission: db.renewSubagentAdmission,
|
|
releaseOwnerAdmission: db.releaseSubagentAdmission,
|
|
...(completionWakeupsEnabled && {
|
|
onTaskPrepared: createSubagentCompletionWakeupHandler(enqueueAgentTrigger),
|
|
}),
|
|
},
|
|
);
|
|
|
|
let taskRoutingConfigured = false;
|
|
|
|
/** Starts the optional Redis owner directory before HTTP admission opens. */
|
|
async function configureSubagentTaskRouting() {
|
|
if (taskRoutingConfigured || !cacheConfig.USE_REDIS) {
|
|
return;
|
|
}
|
|
if (ioredisClient == null || typeof ioredisClient.duplicate !== 'function') {
|
|
throw new Error('Redis subagent task routing requires a dedicated subscriber connection.');
|
|
}
|
|
const subscriber = ioredisClient.duplicate();
|
|
/** A dedicated publisher without the offline queue: the shared client would hold a
|
|
* command issued during a disconnect and deliver it after the caller gave up, so a
|
|
* steer the caller was told had failed could still reach the child. Failing fast
|
|
* turns that into the honest `unavailable` the caller already handles. */
|
|
const publisher = duplicateIoRedisClient(ioredisClient, { enableOfflineQueue: false });
|
|
const transport = new RedisSubagentTaskControlTransport(publisher, subscriber, {
|
|
namespace: cacheConfig.REDIS_KEY_PREFIX,
|
|
});
|
|
try {
|
|
await subagentThreadTaskStore.configureTaskControlTransport(transport);
|
|
} catch (error) {
|
|
subscriber.disconnect();
|
|
publisher.disconnect();
|
|
throw error;
|
|
}
|
|
taskRoutingConfigured = true;
|
|
registerShutdownTask(
|
|
'subagent task control transport',
|
|
async () => {
|
|
await subagentThreadTaskStore.destroyTaskControlTransport();
|
|
publisher.disconnect();
|
|
},
|
|
{ priority: 90 },
|
|
);
|
|
}
|
|
|
|
module.exports = subagentThreadTaskStore;
|
|
module.exports.configureSubagentTaskRouting = configureSubagentTaskRouting;
|