🧷 fix: Fence Server-Admitted Queue Boundaries (#15393)

* fix: fence server-admitted queue boundaries

* fix: preserve effective queue admission boundaries

* fix: fence queue recovery predecessor

* fix: unify queued boundary reconciliation

* fix: make queued turn receipt settlement monotonic

* fix: narrow queued receipt projection status

* fix: close queued turn settlement races

* fix: make queued admission lineage clock independent

* fix: centralize queued turn admission ownership

* test: type queued admission outcomes

* fix: make queued admission identity durable

* fix: carry queued admission identity end to end

* style: sort queued turn test imports

* fix: unify indeterminate admission ownership

* fix: migrate queued admission fence state

* fix: migrate every queued admission owner

* fix: surface queued admission reconciliation

* fix: preserve queued receipt evidence

* test: type queued receipt regression

* fix: stabilize queued receipt authority

* fix: supersede initial receipt fetches
This commit is contained in:
Danny Avila 2026-08-31 12:31:07 -04:00 • committed by GitHub
parent f20b978075
commit f0f82ec5fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 3408 additions and 262 deletions

View file

@ -103,8 +103,18 @@ function QueuedRow({
const quoteCount = message.quotes?.length ?? 0;
const isRecovered = message.recoverySteerId != null;
const isRejected = message.server?.status === 'rejected';
const isIndeterminate = message.server?.status === 'indeterminate';
const isUnconfirmed =
message.server?.status === 'uncertain' && message.server.reconciliationExpired === true;
let statusLabel:
| 'com_ui_queued_turn_reconciliation_required'
| 'com_ui_steer_delivery_unconfirmed'
| 'com_ui_queued_turn_failed' = 'com_ui_queued_turn_failed';
if (isIndeterminate) {
statusLabel = 'com_ui_queued_turn_reconciliation_required';
} else if (isUnconfirmed) {
statusLabel = 'com_ui_steer_delivery_unconfirmed';
}
const requiresDiscard = isRecovered || message.server?.id != null;
const serverActionable =
message.server == null ||
@ -198,7 +208,7 @@ function QueuedRow({
return (
<div role="listitem" className={ROW_CLASS} data-testid="queued-message-row">
{isRejected || isUnconfirmed ? (
{isRejected || isUnconfirmed || isIndeterminate ? (
<TriangleAlert className="h-4 w-4 shrink-0 text-text-warning" aria-hidden="true" />
) : (
<Clock className="h-4 w-4 shrink-0 text-cyan-500" aria-hidden="true" />
@ -216,12 +226,8 @@ function QueuedRow({
0: String(fileCount),
})}
/>
{(isRejected || isUnconfirmed) && (
<span className="shrink-0 text-xs text-text-warning">
{localize(
isUnconfirmed ? 'com_ui_steer_delivery_unconfirmed' : 'com_ui_queued_turn_failed',
)}
</span>
{(isRejected || isUnconfirmed || isIndeterminate) && (
<span className="shrink-0 text-xs text-text-warning">{localize(statusLabel)}</span>
)}
{showPrimary && (
<button

View file

@ -181,6 +181,25 @@ describe('PendingSteerChips — terminal server state', () => {
expect(screen.getByText('com_ui_queued_turn_failed')).toBeInTheDocument();
});
it('shows indeterminate admission as non-actionable reconciliation work', () => {
renderChips([
{
id: 'indeterminate-turn',
text: 'external result unknown',
createdAt: 1,
server: {
id: 'server-indeterminate-1',
status: 'indeterminate',
errorCode: 'ADMISSION_INDETERMINATE',
},
},
]);
expect(screen.getByText('com_ui_queued_turn_reconciliation_required')).toBeInTheDocument();
expect(screen.getByLabelText('com_ui_remove_queued')).toBeDisabled();
expect(screen.queryByText('com_ui_send_now')).toBeNull();
});
it('dismisses an expired ambiguous receipt without restoring or resending it', () => {
renderChips([
{

View file

@ -1,6 +1,18 @@
import { act, renderHook, waitFor } from '@testing-library/react';
const mockListAgentQueuedTurns = jest.fn();
const mockEnqueueAgentQueuedTurn = jest.fn();
const mockCancelAgentQueuedTurn = jest.fn();
const mockRefetch = jest.fn();
const mockCancelQueries = jest.fn().mockResolvedValue(undefined);
const mockQueryClient = { cancelQueries: mockCancelQueries };
const mockUseQuery = jest.fn((options: unknown) => ({ options, refetch: mockRefetch }));
jest.mock('@tanstack/react-query', () => ({
useQuery: (options: unknown) => mockUseQuery(options),
useMutation: jest.fn(),
useQueryClient: () => mockQueryClient,
}));
jest.mock('librechat-data-provider', () => ({
QueryKeys: { agentQueuedTurns: 'agentQueuedTurns' },
@ -21,6 +33,8 @@ import {
fetchAgentQueuedTurns,
isDefiniteQueuedTurnRejection,
isDefiniteQueuedTurnsUnsupported,
shouldPollAgentQueuedTurns,
useAgentQueuedTurns,
} from '../queuedTurns';
describe('Agent queued-turn data adapter', () => {
@ -116,4 +130,87 @@ describe('Agent queued-turn data adapter', () => {
expect(isDefiniteQueuedTurnRejection(error)).toBe(false);
},
);
it('stops background polling for a fail-closed indeterminate claim', () => {
expect(
shouldPollAgentQueuedTurns([
{
status: 'claimed',
failure: { code: 'ADMISSION_INDETERMINATE' },
},
]),
).toBe(false);
expect(shouldPollAgentQueuedTurns([{ status: 'claimed' }])).toBe(true);
expect(shouldPollAgentQueuedTurns([{ status: 'queued' }])).toBe(true);
});
it('refreshes stopped reconciliation work on every mount and focus', () => {
renderHook(() => useAgentQueuedTurns('conversation/one', true));
expect(mockUseQuery).toHaveBeenCalledWith(
expect.objectContaining({
refetchOnMount: 'always',
refetchOnWindowFocus: 'always',
}),
);
});
it('keeps one cache authority and refetches when known receipt ids change', async () => {
const rendered = renderHook(
({ ids }: { ids: string[] }) => useAgentQueuedTurns('conversation/one', true, ids),
{ initialProps: { ids: ['request-one'] } },
);
expect(mockUseQuery).toHaveBeenLastCalledWith(
expect.objectContaining({
queryKey: ['agentQueuedTurns', 'conversation/one'],
}),
);
expect(mockRefetch).not.toHaveBeenCalled();
rendered.rerender({ ids: [] });
await waitFor(() =>
expect(mockCancelQueries).toHaveBeenCalledWith({
queryKey: ['agentQueuedTurns', 'conversation/one'],
exact: true,
}),
);
await waitFor(() =>
expect(mockRefetch).toHaveBeenCalledWith({
cancelRefetch: true,
}),
);
expect(mockCancelQueries.mock.invocationCallOrder[0]).toBeLessThan(
mockRefetch.mock.invocationCallOrder[0],
);
expect(mockUseQuery).toHaveBeenLastCalledWith(
expect.objectContaining({
queryKey: ['agentQueuedTurns', 'conversation/one'],
}),
);
});
it('lets only the newest identity projection refetch after cancellation', async () => {
let releaseFirstCancellation: (() => void) | undefined;
mockCancelQueries
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
releaseFirstCancellation = resolve;
}),
)
.mockResolvedValueOnce(undefined);
const rendered = renderHook(
({ ids }: { ids: string[] }) => useAgentQueuedTurns('conversation/one', true, ids),
{ initialProps: { ids: ['request-one'] } },
);
rendered.rerender({ ids: ['request-two'] });
rendered.rerender({ ids: ['request-three'] });
await waitFor(() => expect(mockRefetch).toHaveBeenCalledTimes(1));
await act(async () => releaseFirstCancellation?.());
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, QueryKeys } from 'librechat-data-provider';
import type {
@ -8,10 +9,12 @@ import type {
export type AgentQueuedTurnReceipt = TAgentQueuedTurnReceipt;
export type EnqueueAgentQueuedTurnRequest = TEnqueueAgentQueuedTurnRequest;
export const agentQueuedTurnsQueryKey = (
conversationId: string,
clientRequestIds: readonly string[] = [],
) => [QueryKeys.agentQueuedTurns, conversationId, ...clientRequestIds] as const;
/** Keep one cache authority for a conversation. Known receipt ids change the
* server projection, not the identity of the durable queue. Giving every id
* set its own key can resurrect an older cached projection after an id is
* retired. */
export const agentQueuedTurnsQueryKey = (conversationId: string) =>
[QueryKeys.agentQueuedTurns, conversationId] as const;
function queuedTurnErrorResponse(error: unknown): { status?: number; code?: string } {
const response = (
@ -72,29 +75,85 @@ export async function cancelAgentQueuedTurn(input: {
return response.receipt;
}
/** Indeterminate admission deliberately retains its durable claim until exact
* source evidence arrives. It can still be refreshed on mount/focus, but a
* two-second loop cannot resolve it and disguises permanent quarantine as
* ordinary progress. */
export function shouldPollAgentQueuedTurns(
receipts: unknown,
reconcileUntil?: number,
observedAt = Date.now(),
): boolean {
return (
(reconcileUntil != null && observedAt < reconcileUntil) ||
(Array.isArray(receipts) &&
receipts.some(
(item: AgentQueuedTurnReceipt) =>
item.status === 'queued' ||
(item.status === 'claimed' && item.failure?.code !== 'ADMISSION_INDETERMINATE'),
))
);
}
export function useAgentQueuedTurns(
conversationId: string,
enabled: boolean,
clientRequestIds: string[] = [],
reconcileUntil?: number,
) {
const queryClient = useQueryClient();
const knownIds = [...new Set(clientRequestIds)].sort();
return useQuery({
queryKey: agentQueuedTurnsQueryKey(conversationId, knownIds),
const knownIdsSignature = knownIds.join('\u0000');
const previousRequest = useRef({ conversationId, knownIdsSignature });
const query = useQuery({
queryKey: agentQueuedTurnsQueryKey(conversationId),
queryFn: () => fetchAgentQueuedTurns(conversationId, knownIds),
enabled: enabled && conversationId.length > 0,
staleTime: 1_000,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: (receipts) => {
return (reconcileUntil != null && Date.now() < reconcileUntil) ||
(Array.isArray(receipts) &&
receipts.some((item) => item.status === 'queued' || item.status === 'claimed'))
? 2_000
: false;
},
/** A stopped indeterminate row can receive exact proof at any time. Its
* cache may still be inside `staleTime`, so mount/focus must bypass the
* ordinary freshness gate rather than waiting for another user cycle. */
refetchOnMount: 'always',
refetchOnWindowFocus: 'always',
refetchInterval: (receipts) =>
shouldPollAgentQueuedTurns(receipts, reconcileUntil) ? 2_000 : false,
retry: false,
});
const { refetch } = query;
useEffect(() => {
const previous = previousRequest.current;
previousRequest.current = { conversationId, knownIdsSignature };
if (
!enabled ||
conversationId.length === 0 ||
previous.conversationId !== conversationId ||
previous.knownIdsSignature === knownIdsSignature
) {
return;
}
let superseded = false;
const refreshProjection = async () => {
/** React Query cannot replace an initial fetch that has no cached data
* through `refetch` alone. Cancel it explicitly so the next request
* captures the current reconciliation identities. */
await queryClient.cancelQueries({
queryKey: agentQueuedTurnsQueryKey(conversationId),
exact: true,
});
if (!superseded) {
await refetch({ cancelRefetch: true });
}
};
void refreshProjection();
return () => {
superseded = true;
};
}, [conversationId, enabled, knownIdsSignature, queryClient, refetch]);
return query;
}
export function useEnqueueAgentQueuedTurnMutation() {

View file

@ -3,7 +3,12 @@ import { Constants } from 'librechat-data-provider';
import { act, renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RecoilRoot, useRecoilValue, useSetRecoilState, type MutableSnapshot } from 'recoil';
import type { DrainAfterAbort, RunEnd, QueuedMessage } from '~/store/families';
import type {
DrainAfterAbort,
RunEnd,
QueuedMessage,
SettledQueuedTurnReceipt,
} from '~/store/families';
import useQueueDrain from '../useQueueDrain';
import store from '~/store';
@ -26,8 +31,12 @@ function setup(
setIsSubmitting?: (value: boolean) => void;
setQueue?: (value: QueuedMessage[]) => void;
setNewConvoQueue?: (value: QueuedMessage[]) => void;
setSettledReceipts?: (value: SettledQueuedTurnReceipt[]) => void;
setInterruptFlag?: (value: DrainAfterAbort | false) => void;
queueRef?: { current: QueuedMessage[] };
queue?: QueuedMessage[];
newConvoQueue?: QueuedMessage[];
settledReceipts?: SettledQueuedTurnReceipt[];
runEnd?: RunEnd | null;
} = {};
function Harness() {
@ -37,7 +46,14 @@ function setup(
setters.setNewConvoQueue = useSetRecoilState(
store.queuedMessagesByConvoId(Constants.NEW_CONVO),
);
setters.setSettledReceipts = useSetRecoilState(
store.settledQueuedTurnReceiptsByConvoId(CONVO_ID),
);
setters.setInterruptFlag = useSetRecoilState(store.drainAfterAbortByIndex(INDEX));
setters.queue = useRecoilValue(store.queuedMessagesByConvoId(CONVO_ID));
setters.newConvoQueue = useRecoilValue(store.queuedMessagesByConvoId(Constants.NEW_CONVO));
setters.settledReceipts = useRecoilValue(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID));
setters.runEnd = useRecoilValue(store.runEndByIndex(INDEX));
useQueueDrain(INDEX, activeConversationId, ask);
return null;
}
@ -150,6 +166,191 @@ describe('useQueueDrain', () => {
expect(ask).toHaveBeenCalledWith({ text: 'legacy successor' }, emptyOverrides);
});
it('reacts to a settled admission while another server row still owns the queue', async () => {
const admittedServer: QueuedMessage = {
...queuedMessage('q-server-1', 'admitted server-owned turn'),
server: { id: 'server-queue-1', status: 'claimed', revision: 1 },
};
const remainingServer: QueuedMessage = {
...queuedMessage('q-server-2', 'later server-owned turn'),
server: { id: 'server-queue-2', status: 'queued', revision: 2 },
};
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [admittedServer, remainingServer]);
});
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 41 }));
});
await new Promise((resolve) => setTimeout(resolve, 25));
expect(setters.runEnd).not.toBeNull();
act(() => {
setters.setQueue!([remainingServer]);
setters.setSettledReceipts!([
{
clientRequestId: 'admitted-request-1',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
await waitFor(() => expect(setters.runEnd).toBeNull());
expect(ask).not.toHaveBeenCalled();
expect(setters.settledReceipts).toEqual([]);
expect(setters.queue).toEqual([remainingServer]);
});
it('discards a predecessor boundary already consumed by server admission', async () => {
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [
queuedMessage('q-local', 'must wait for the admitted run'),
]);
set(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID), [
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 41 }));
});
await waitFor(() => expect(setters.runEnd).toBeNull());
expect(ask).not.toHaveBeenCalled();
expect(setters.settledReceipts).toEqual([]);
});
it('consumes one admission when separate receipts share the same predecessor epoch', async () => {
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [
queuedMessage('q-local', 'wait for both admitted runs'),
]);
set(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID), [
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
{
clientRequestId: 'admission-2',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 41 }));
});
await waitFor(() =>
expect(setters.settledReceipts).toEqual([
{
clientRequestId: 'admission-2',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]),
);
expect(ask).not.toHaveBeenCalled();
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 41, endedAt: Date.now() + 1 }));
});
await waitFor(() => expect(setters.settledReceipts).toEqual([]));
expect(ask).not.toHaveBeenCalled();
});
it('migrates the NEW_CONVO queue before discarding a consumed predecessor boundary', async () => {
const queuedBeforeResolution = queuedMessage('q-new', 'wait for the admitted successor');
const queuedAfterResolution = queuedMessage('q-resolved', 'still ordered after migration');
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(Constants.NEW_CONVO), [queuedBeforeResolution]);
set(store.queuedMessagesByConvoId(CONVO_ID), [queuedAfterResolution]);
set(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID), [
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
act(() => {
setters.setRunEnd!(
runEnd({
generationCreatedAt: 41,
startedAsNewConvo: true,
}),
);
});
await waitFor(() => expect(setters.runEnd).toBeNull());
expect(ask).not.toHaveBeenCalled();
expect(setters.newConvoQueue).toEqual([]);
expect(setters.queue).toEqual([queuedBeforeResolution, queuedAfterResolution]);
});
it('lets the admitted successor terminal boundary release the next turn', async () => {
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [
queuedMessage('q-local', 'send after the admitted run'),
]);
set(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID), [
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 42 }));
});
await waitFor(() => expect(ask).toHaveBeenCalledTimes(1));
expect(ask).toHaveBeenCalledWith({ text: 'send after the admitted run' }, emptyOverrides);
});
it('does not interpret a consumed predecessor as a timestamp range', async () => {
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [
queuedMessage('q-local', 'send after the lower-clock successor'),
]);
set(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID), [
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 42,
},
]);
});
act(() => {
setters.setRunEnd!(runEnd({ generationCreatedAt: 41 }));
});
await waitFor(() => expect(ask).toHaveBeenCalledTimes(1));
expect(ask).toHaveBeenCalledWith(
{ text: 'send after the lower-clock successor' },
emptyOverrides,
);
expect(setters.settledReceipts).toEqual([
{
clientRequestId: 'admission-1',
status: 'admitted',
effectivePredecessorCreatedAt: 42,
},
]);
});
it('parks a mismatched signal instead of draining into the wrong conversation', async () => {
const { ask, setters } = setup(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [queuedMessage('q1', 'stay put')]);

View file

@ -4,6 +4,7 @@ import { RecoilRoot, useRecoilValue, useSetRecoilState, type MutableSnapshot } f
import { Constants, ContentTypes, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { TConversation, TMessage } from 'librechat-data-provider';
import type { QueuedMessage } from '~/store/families';
import useQueueDrain from '../useQueueDrain';
import useSteering from '../useSteering';
import store from '~/store';
@ -260,6 +261,8 @@ describe('useSteering', () => {
stopGenerating: jest.fn(),
}),
queue: useQueue(CONVO_ID),
settledReceipts: useRecoilValue(store.settledQueuedTurnReceiptsByConvoId(CONVO_ID)),
pendingEnqueueIds: useRecoilValue(store.pendingQueuedTurnEnqueueIdsByConvoId(CONVO_ID)),
}),
{ wrapper },
);
@ -335,6 +338,377 @@ describe('useSteering', () => {
);
});
it('applies an admitted POST replay to the consumed predecessor set', async () => {
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
options.onSuccess({
...input,
queuedTurnId: 'server-admitted-replay',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
});
});
const { result } = setupServerQueue(
withActiveGeneration(({ set }) => {
set(store.queuedMessagesByConvoId(CONVO_ID), [
{
id: 'unrelated-server-row',
clientRequestId: 'unrelated-request',
text: 'keep me',
createdAt: 50,
server: { id: 'unrelated-server-id', status: 'queued', revision: 1 },
},
]);
}),
);
await act(async () => {
result.current.steering.queueFromComposer('already admitted');
await Promise.resolve();
});
await waitFor(() =>
expect(result.current.settledReceipts).toEqual([
{
clientRequestId: expect.stringMatching(UUID_V4_PATTERN),
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]),
);
expect(result.current.queue).toEqual([
expect.objectContaining({ id: 'unrelated-server-row', text: 'keep me' }),
]);
});
it('keeps an old-server admitted receipt uncertain until its exact boundary is known', async () => {
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
options.onSuccess({
...input,
queuedTurnId: 'legacy-admitted-replay',
status: 'admitted',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
});
});
const { result } = setupServerQueue();
await act(async () => {
result.current.steering.queueFromComposer('await exact admission boundary');
await Promise.resolve();
});
await waitFor(() =>
expect(result.current.queue).toEqual([
expect.objectContaining({
text: 'await exact admission boundary',
clientRequestId: expect.stringMatching(UUID_V4_PATTERN),
server: expect.objectContaining({
id: 'legacy-admitted-replay',
status: 'uncertain',
}),
}),
]),
);
expect(result.current.settledReceipts).toEqual([]);
expect(mockUseAgentQueuedTurns.mock.calls.at(-1)?.[2]).toEqual([
result.current.queue[0].clientRequestId,
]);
});
it('does not let a stale queued POST erase incomplete admission evidence', async () => {
let releaseQueuedPost = () => undefined;
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
releaseQueuedPost = () =>
options.onSuccess({
...input,
queuedTurnId: 'legacy-admitted-race',
status: 'queued',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(100).toISOString(),
});
});
const rendered = setupServerQueue();
await act(async () => {
rendered.result.current.steering.queueFromComposer('hold incomplete admission');
await Promise.resolve();
});
await waitFor(() => expect(mockEnqueueQueuedTurn).toHaveBeenCalledTimes(1));
const input = mockEnqueueQueuedTurn.mock.calls[0][0];
mockServerQueuedTurns = [
{
...input,
queuedTurnId: 'legacy-admitted-race',
status: 'admitted',
revision: 2,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
},
];
rendered.rerender();
await waitFor(() =>
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'legacy-admitted-race',
status: 'uncertain',
}),
);
expect(rendered.result.current.settledReceipts).toEqual([
{ clientRequestId: input.clientRequestId, status: 'admitted_pending_boundary' },
]);
await act(async () => {
releaseQueuedPost();
await Promise.resolve();
});
expect(rendered.result.current.queue).toHaveLength(1);
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'legacy-admitted-race',
status: 'uncertain',
});
expect(rendered.result.current.pendingEnqueueIds).toEqual([]);
expect(rendered.result.current.settledReceipts).toEqual([]);
});
it('does not let a stale queued POST response resurrect a terminal receipt', async () => {
let releaseQueuedPost = () => undefined;
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
releaseQueuedPost = () =>
options.onSuccess({
...input,
queuedTurnId: 'server-stale-post',
status: 'queued',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(100).toISOString(),
});
});
const rendered = setupServerQueue();
await act(async () => {
rendered.result.current.steering.queueFromComposer('settle before POST returns');
await Promise.resolve();
});
await waitFor(() => expect(mockEnqueueQueuedTurn).toHaveBeenCalledTimes(1));
const input = mockEnqueueQueuedTurn.mock.calls[0][0];
mockServerQueuedTurns = [
{
...input,
queuedTurnId: 'server-terminal-get',
status: 'admitted',
effectivePredecessorCreatedAt: 41,
revision: 2,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
},
];
rendered.rerender();
await waitFor(() => expect(rendered.result.current.queue).toEqual([]));
await act(async () => {
releaseQueuedPost();
await Promise.resolve();
});
expect(rendered.result.current.queue).toEqual([]);
expect(rendered.result.current.settledReceipts).toEqual([
{
clientRequestId: input.clientRequestId,
status: 'admitted',
effectivePredecessorCreatedAt: 41,
},
]);
});
it('does not let a stale queued POST response resurrect a cancelled row', async () => {
let releaseQueuedPost = () => undefined;
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
releaseQueuedPost = () =>
options.onSuccess({
...input,
queuedTurnId: 'server-cancel-race',
status: 'queued',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(100).toISOString(),
});
});
const rendered = setupServerQueue();
await act(async () => {
rendered.result.current.steering.queueFromComposer('cancel before POST returns');
await Promise.resolve();
});
await waitFor(() => expect(mockEnqueueQueuedTurn).toHaveBeenCalledTimes(1));
const input = mockEnqueueQueuedTurn.mock.calls[0][0];
mockServerQueuedTurns = [
{
...input,
queuedTurnId: 'server-cancel-race',
status: 'queued',
revision: 2,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
},
];
rendered.rerender();
await waitFor(() =>
expect(rendered.result.current.queue[0].server?.id).toBe('server-cancel-race'),
);
mockCancelQueuedTurn.mockResolvedValueOnce({
...input,
queuedTurnId: 'server-cancel-race',
status: 'cancelled',
revision: 3,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(300).toISOString(),
});
await act(async () => {
await rendered.result.current.steering.discardQueued(rendered.result.current.queue[0]);
});
expect(rendered.result.current.queue[0].server).toBeUndefined();
expect(rendered.result.current.settledReceipts).toEqual([
{ clientRequestId: input.clientRequestId, status: 'cancelled' },
]);
await act(async () => {
releaseQueuedPost();
await Promise.resolve();
});
expect(rendered.result.current.queue).toEqual([
expect.objectContaining({
text: 'cancel before POST returns',
clientRequestId: input.clientRequestId,
}),
]);
expect(rendered.result.current.queue[0].server).toBeUndefined();
expect(rendered.result.current.pendingEnqueueIds).toEqual([]);
expect(rendered.result.current.settledReceipts).toEqual([]);
});
it('does not let a stale queued POST response replace a definitive dead snapshot', async () => {
let releaseQueuedPost = () => undefined;
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
releaseQueuedPost = () =>
options.onSuccess({
...input,
queuedTurnId: 'server-dead-race',
status: 'queued',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(100).toISOString(),
});
});
const rendered = setupServerQueue();
await act(async () => {
rendered.result.current.steering.queueFromComposer('fail before POST returns');
await Promise.resolve();
});
await waitFor(() => expect(mockEnqueueQueuedTurn).toHaveBeenCalledTimes(1));
const input = mockEnqueueQueuedTurn.mock.calls[0][0];
mockServerQueuedTurns = [
{
...input,
queuedTurnId: 'server-dead-race',
status: 'dead',
revision: 2,
failure: { code: 'PREDECESSOR_ABORTED', message: 'The predecessor aborted' },
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
},
];
rendered.rerender();
await waitFor(() =>
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'server-dead-race',
status: 'rejected',
errorCode: 'PREDECESSOR_ABORTED',
}),
);
expect(rendered.result.current.settledReceipts).toEqual([
{ clientRequestId: input.clientRequestId, status: 'dead' },
]);
await act(async () => {
releaseQueuedPost();
await Promise.resolve();
});
expect(rendered.result.current.queue).toHaveLength(1);
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'server-dead-race',
status: 'rejected',
errorCode: 'PREDECESSOR_ABORTED',
});
expect(rendered.result.current.pendingEnqueueIds).toEqual([]);
expect(rendered.result.current.settledReceipts).toEqual([]);
});
it('does not let a stale queued POST replace indeterminate GET evidence', async () => {
let releaseQueuedPost = () => undefined;
mockEnqueueQueuedTurn.mockImplementation((input, options) => {
releaseQueuedPost = () =>
options.onSuccess({
...input,
queuedTurnId: 'server-indeterminate-race',
status: 'queued',
revision: 1,
createdAt: new Date(100).toISOString(),
updatedAt: new Date(100).toISOString(),
});
});
const rendered = setupServerQueue();
await act(async () => {
rendered.result.current.steering.queueFromComposer('unknown before POST returns');
await Promise.resolve();
});
await waitFor(() => expect(mockEnqueueQueuedTurn).toHaveBeenCalledTimes(1));
const input = mockEnqueueQueuedTurn.mock.calls[0][0];
mockServerQueuedTurns = [
{
...input,
queuedTurnId: 'server-indeterminate-race',
status: 'claimed',
revision: 2,
failure: { code: 'ADMISSION_INDETERMINATE' },
createdAt: new Date(100).toISOString(),
updatedAt: new Date(200).toISOString(),
},
];
rendered.rerender();
await waitFor(() =>
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'server-indeterminate-race',
status: 'indeterminate',
revision: 2,
}),
);
expect(rendered.result.current.settledReceipts).toEqual([
{ clientRequestId: input.clientRequestId, status: 'indeterminate' },
]);
await act(async () => {
releaseQueuedPost();
await Promise.resolve();
});
expect(rendered.result.current.queue[0].server).toMatchObject({
id: 'server-indeterminate-race',
status: 'indeterminate',
revision: 2,
});
expect(rendered.result.current.pendingEnqueueIds).toEqual([]);
expect(rendered.result.current.settledReceipts).toEqual([]);
});
it('anchors enqueue to the selected branch tail instead of a later hidden sibling', async () => {
mockMessages = [
{
@ -482,16 +856,144 @@ describe('useSteering', () => {
updatedAt: new Date(200).toISOString(),
},
];
const { result } = setupServerQueue();
const rendered = setupServerQueue();
await waitFor(() => expect(result.current.queue).toHaveLength(1));
expect(result.current.queue[0]).toMatchObject({
await waitFor(() => expect(rendered.result.current.queue).toHaveLength(1));
expect(rendered.result.current.queue[0]).toMatchObject({
id: 'client-snapshot-1',
text: 'restored after reload',
server: { id: 'server-snapshot-1', status: 'queued', revision: 3 },
});
});
it('fences the predecessor boundary when the server receipt is admitted', async () => {
mockServerQueuedTurns = [
{
queuedTurnId: 'server-admitted-1',
clientRequestId: 'client-admitted-1',
conversationId: CONVO_ID,
parentMessageId: 'visible-assistant-tail',
text: 'already started by the server',
status: 'admitted',
revision: 4,
expectedPredecessorCreatedAt: 41,
effectivePredecessorCreatedAt: 42,
createdAt: new Date(200).toISOString(),
updatedAt: new Date(300).toISOString(),
},
];
const { result } = setupServerQueue();
await waitFor(() =>
expect(result.current.settledReceipts).toEqual([
{
clientRequestId: 'client-admitted-1',
status: 'admitted',
effectivePredecessorCreatedAt: 42,
},
]),
);
expect(result.current.queue).toEqual([]);
});
it('settles an explicit root admission without waiting for a timestamp boundary', async () => {
mockServerQueuedTurns = [
{
queuedTurnId: 'server-root-admitted',
clientRequestId: 'client-root-admitted',
conversationId: CONVO_ID,
parentMessageId: 'visible-assistant-tail',
text: 'started from the conversation root',
status: 'admitted',
revision: 4,
rootPredecessor: true,
createdAt: new Date(200).toISOString(),
updatedAt: new Date(300).toISOString(),
},
];
const { result } = setupServerQueue();
await waitFor(() => expect(result.current.queue).toEqual([]));
expect(result.current.settledReceipts).toEqual([]);
});
it('keeps the terminal boundary when an old-server admission lacks its exact fence', async () => {
mockServerQueuedTurns = [
{
queuedTurnId: 'server-admitted-race',
clientRequestId: 'client-admitted-race',
conversationId: CONVO_ID,
parentMessageId: 'visible-assistant-tail',
text: 'server-owned successor',
status: 'admitted',
revision: 4,
expectedPredecessorCreatedAt: 41,
createdAt: new Date(200).toISOString(),
updatedAt: new Date(300).toISOString(),
},
];
const ask = jest.fn();
const sendNow = jest.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot
initializeState={withActiveGeneration(({ set }) => {
set(store.isSubmittingFamily(0), false);
set(store.runEndByIndex(0), {
conversationId: CONVO_ID,
outcome: 'completed',
endedAt: 200,
generationCreatedAt: 41,
});
set(store.queuedMessagesByConvoId(CONVO_ID), [
{
id: 'client-admitted-race',
clientRequestId: 'client-admitted-race',
text: 'server-owned successor',
createdAt: 100,
server: { id: 'server-admitted-race', status: 'claimed', revision: 3 },
},
{ id: 'local-successor', text: 'must wait', createdAt: 101 },
]);
})}
>
{children}
</RecoilRoot>
);
const { result } = renderHook(
() => {
useSteering({
index: 0,
conversationId: CONVO_ID,
conversation: agentsConversation,
isSubmitting: false,
answerModeActive: false,
sendNow,
stopGenerating: jest.fn(),
});
useQueueDrain(0, CONVO_ID, ask);
return {
queue: useQueue(CONVO_ID),
runEnd: useRecoilValue(store.runEndByIndex(0)),
};
},
{ wrapper },
);
await waitFor(() =>
expect(result.current.queue[0]).toMatchObject({
id: 'client-admitted-race',
server: { id: 'server-admitted-race', status: 'uncertain' },
}),
);
expect(result.current.runEnd).not.toBeNull();
expect(result.current.queue.map((item) => item.id)).toEqual([
'client-admitted-race',
'local-successor',
]);
expect(ask).not.toHaveBeenCalled();
expect(sendNow).not.toHaveBeenCalled();
});
it('uses server sequence instead of Mongo timestamps for projected queue order', async () => {
mockServerQueuedTurns = [
{
@ -542,10 +1044,10 @@ describe('useSteering', () => {
},
];
mockCancelQueuedTurn.mockResolvedValueOnce({ status: 'cancelled' });
const { result } = setupServerQueue();
const rendered = setupServerQueue();
await waitFor(() => expect(result.current.queue).toHaveLength(1));
expect(result.current.queue[0]).toMatchObject({
await waitFor(() => expect(rendered.result.current.queue).toHaveLength(1));
expect(rendered.result.current.queue[0]).toMatchObject({
text: 'recover this failed turn',
server: {
id: 'server-dead-1',
@ -556,15 +1058,48 @@ describe('useSteering', () => {
});
await act(async () => {
await expect(result.current.steering.discardQueued(result.current.queue[0])).resolves.toBe(
true,
);
await expect(
rendered.result.current.steering.discardQueued(rendered.result.current.queue[0]),
).resolves.toBe(true);
});
expect(mockCancelQueuedTurn).toHaveBeenCalledWith({
conversationId: CONVO_ID,
queuedTurnId: 'server-dead-1',
});
expect(result.current.queue[0].server).toBeUndefined();
expect(rendered.result.current.queue[0].server).toBeUndefined();
});
it('projects an indeterminate claim as non-progressing reconciliation work', async () => {
mockServerQueuedTurns = [
{
queuedTurnId: 'server-indeterminate-1',
clientRequestId: 'client-indeterminate-1',
conversationId: CONVO_ID,
parentMessageId: 'visible-assistant-tail',
text: 'external result is unknown',
status: 'claimed',
revision: 4,
failure: {
code: 'ADMISSION_INDETERMINATE',
message: 'Awaiting exact source evidence',
},
createdAt: new Date(250).toISOString(),
updatedAt: new Date(300).toISOString(),
},
];
const rendered = setupServerQueue();
await waitFor(() => expect(rendered.result.current.queue).toHaveLength(1));
expect(rendered.result.current.queue[0]).toMatchObject({
text: 'external result is unknown',
server: {
id: 'server-indeterminate-1',
status: 'indeterminate',
errorCode: 'ADMISSION_INDETERMINATE',
errorMessage: 'Awaiting exact source evidence',
},
});
expect(rendered.result.current.settledReceipts).toEqual([]);
});
it('cancels the durable copy before downgrading a row to local control', async () => {

View file

@ -89,6 +89,13 @@ export default function useQueueDrain(
* during the first turn stay keyed here until that run ends. Renewing only
* the active id would skip them for the whole of that run. */
const newConvoQueue = useRecoilValue(store.queuedMessagesByConvoId(Constants.NEW_CONVO));
/** Receipt settlement can consume the parked terminal boundary without
* changing whether another server-owned row remains. Subscribe here so the
* drain effect observes that durable transition instead of reading it only
* through a callback snapshot whose other dependencies stayed unchanged. */
const settledQueuedTurnReceipts = useRecoilValue(
store.settledQueuedTurnReceiptsByConvoId(activeConversationId ?? Constants.NEW_CONVO),
);
const hasServerOwnedQueue = [...ownQueue, ...newConvoQueue].some((item) => item.server != null);
/* Deduped because the two subscriptions are the same atom before migration.
@ -238,6 +245,57 @@ export default function useQueueDrain(
: ownQueue;
const shouldDrain = end.outcome === 'completed' || interruptArmed;
const settledReceipts = snapshot
.getLoadable(store.settledQueuedTurnReceiptsByConvoId(conversationId))
.getValue();
const pendingEnqueueIds = snapshot
.getLoadable(store.pendingQueuedTurnEnqueueIdsByConvoId(conversationId))
.getValue();
const consumedReceiptIndex = settledReceipts.findIndex(
(receipt) =>
receipt.status === 'admitted' &&
receipt.boundaryConsumed !== true &&
end.generationCreatedAt != null &&
receipt.effectivePredecessorCreatedAt === end.generationCreatedAt,
);
const consumedByServerAdmission = consumedReceiptIndex >= 0;
const consumeEnd = () => {
if (fromParked && activeConversationId) {
set(store.pendingRunEndByConvoId(activeConversationId), null);
} else {
set(store.runEndByIndex(index), null);
}
if (matchingIndexArm) {
set(store.drainAfterAbortByIndex(index), false);
}
};
if (consumedByServerAdmission) {
/** Admission consumed this terminal boundary on the server. A late
* client terminal observation cannot authorize a second successor. */
if (shouldMigrate && newConvoQueue.length > 0) {
set(store.queuedMessagesByConvoId(Constants.NEW_CONVO), []);
set(store.queuedMessagesByConvoId(conversationId), merged);
}
set(store.settledQueuedTurnReceiptsByConvoId(conversationId), (previous) => {
let consumed = false;
return previous.flatMap((receipt) => {
if (
!consumed &&
receipt.status === 'admitted' &&
receipt.boundaryConsumed !== true &&
receipt.effectivePredecessorCreatedAt === end.generationCreatedAt
) {
consumed = true;
return pendingEnqueueIds.includes(receipt.clientRequestId)
? [{ ...receipt, boundaryConsumed: true }]
: [];
}
return [receipt];
});
});
consumeEnd();
return null;
}
/** A server-owned Agent row means the backend owns the next fresh-turn
* admission. Do not let a legacy/recovered local row race or overtake
* it. Keep this terminal boundary available until the authoritative
@ -253,14 +311,7 @@ export default function useQueueDrain(
// Consume only after server authority has yielded the boundary — a
// hard double-fire guard even if the effect re-runs before propagation.
if (fromParked && activeConversationId) {
set(store.pendingRunEndByConvoId(activeConversationId), null);
} else {
set(store.runEndByIndex(index), null);
}
if (matchingIndexArm) {
set(store.drainAfterAbortByIndex(index), false);
}
consumeEnd();
const next = shouldDrain ? (merged[0] ?? null) : null;
const remainder = next ? merged.slice(1) : merged;
@ -362,6 +413,7 @@ export default function useQueueDrain(
restoreQueued,
markFilesUsage,
hasServerOwnedQueue,
settledQueuedTurnReceipts,
ask,
]);
}

View file

@ -14,7 +14,13 @@ import type {
TConversation,
TMessageContentParts,
} from 'librechat-data-provider';
import type { RunEnd, PendingSteer, QueuedMessage, QueuedMessageOrigin } from '~/store/families';
import type {
RunEnd,
PendingSteer,
QueuedMessage,
QueuedMessageOrigin,
SettledQueuedTurnReceipt,
} from '~/store/families';
import type { AgentQueuedTurnReceipt, GenerationProtocolVersion } from '~/data-provider';
import type { ExtendedFile, FileSetter } from '~/common';
import {
@ -172,6 +178,76 @@ function queuedTurnCreatedAt(receipt: AgentQueuedTurnReceipt): number {
return Number.isFinite(createdAt) ? createdAt : 0;
}
type QueuedTurnReceiptSource = 'snapshot' | 'enqueue' | 'direct';
function isAdmissionIndeterminateReceipt(receipt: AgentQueuedTurnReceipt): boolean {
return (
receipt.failure?.code === 'ADMISSION_INDETERMINATE' &&
(receipt.status === 'claimed' || receipt.status === 'dead')
);
}
function settledEvidenceForReceipt(
receipt: AgentQueuedTurnReceipt,
): SettledQueuedTurnReceipt | undefined {
if (
receipt.status === 'admitted' &&
(receipt.effectivePredecessorCreatedAt != null || receipt.rootPredecessor === true)
) {
return {
clientRequestId: receipt.clientRequestId,
status: 'admitted',
...(receipt.effectivePredecessorCreatedAt != null && {
effectivePredecessorCreatedAt: receipt.effectivePredecessorCreatedAt,
}),
...(receipt.rootPredecessor === true && { rootPredecessor: true }),
};
}
if (receipt.status === 'admitted') {
return {
clientRequestId: receipt.clientRequestId,
status: 'admitted_pending_boundary',
};
}
if (isAdmissionIndeterminateReceipt(receipt)) {
return { clientRequestId: receipt.clientRequestId, status: 'indeterminate' };
}
if (receipt.status === 'cancelled') {
return { clientRequestId: receipt.clientRequestId, status: 'cancelled' };
}
if (receipt.status === 'dead') {
return { clientRequestId: receipt.clientRequestId, status: 'dead' };
}
return undefined;
}
/** Client receipt knowledge is a monotonic evidence lattice. Enqueue is the
* only source that can carry a pre-scheduling snapshot, so it never weakens
* evidence already observed by GET/direct settlement. Indeterminate evidence
* can advance only through a later authoritative terminal observation. */
function mergeSettledQueuedTurnEvidence(
existing: SettledQueuedTurnReceipt | undefined,
receipt: AgentQueuedTurnReceipt,
source: QueuedTurnReceiptSource,
): SettledQueuedTurnReceipt | undefined {
const incoming = settledEvidenceForReceipt(receipt);
if (existing == null) {
return incoming;
}
if (existing.status === 'admitted_pending_boundary' && incoming?.status === 'admitted') {
return incoming;
}
if (
existing.status === 'indeterminate' &&
source !== 'enqueue' &&
incoming != null &&
incoming.status !== 'indeterminate'
) {
return incoming;
}
return existing;
}
function toQueuedTurnFileRefs(files: TMessage['files']): TAgentQueuedTurnFileRef[] | undefined {
const refs = (files ?? []).flatMap((file): TAgentQueuedTurnFileRef[] => {
if (typeof file.file_id !== 'string' || file.file_id.length === 0) {
@ -195,6 +271,8 @@ function toQueuedTurnFileRefs(files: TMessage['files']): TAgentQueuedTurnFileRef
function reconcileServerQueuedTurns(
previous: QueuedMessage[],
receipts: AgentQueuedTurnReceipt[],
settledByRequestId: ReadonlyMap<string, SettledQueuedTurnReceipt>,
authoritativeSnapshot = true,
): QueuedMessage[] {
const previousByClientRequestId = new Map(
previous.flatMap((item) =>
@ -203,10 +281,30 @@ function reconcileServerQueuedTurns(
);
const observedClientRequestIds = new Set(receipts.map((receipt) => receipt.clientRequestId));
const projected = receipts.flatMap((receipt): QueuedMessage[] => {
if (receipt.status === 'admitted' || receipt.status === 'cancelled') {
const settled = settledByRequestId.get(receipt.clientRequestId);
const admissionIndeterminate = isAdmissionIndeterminateReceipt(receipt);
if (settled?.status === 'admitted' || settled?.status === 'cancelled') {
return [];
}
const optimistic = previousByClientRequestId.get(receipt.clientRequestId);
if (
(settled?.status === 'dead' ||
settled?.status === 'admitted_pending_boundary' ||
(settled?.status === 'indeterminate' && !admissionIndeterminate)) &&
receipt.status !== 'dead' &&
receipt.status !== 'admitted'
) {
return optimistic == null ? [] : [optimistic];
}
const boundaryPending = receipt.status === 'admitted';
let status: NonNullable<QueuedMessage['server']>['status'] = 'rejected';
if (admissionIndeterminate) {
status = 'indeterminate';
} else if (boundaryPending) {
status = 'uncertain';
} else if (receipt.status === 'queued' || receipt.status === 'claimed') {
status = receipt.status;
}
return [
{
id: optimistic?.id ?? receipt.clientRequestId,
@ -226,8 +324,11 @@ function reconcileServerQueuedTurns(
...(receipt.priority === true && { priority: true }),
server: {
id: receipt.queuedTurnId,
status: receipt.status === 'dead' ? 'rejected' : receipt.status,
status,
revision: receipt.revision,
...(boundaryPending && {
uncertainSince: optimistic?.server?.uncertainSince ?? Date.now(),
}),
...(receipt.position != null && { position: receipt.position }),
...(receipt.failure?.code != null && { errorCode: receipt.failure.code }),
...(receipt.failure?.message != null && { errorMessage: receipt.failure.message }),
@ -239,12 +340,24 @@ function reconcileServerQueuedTurns(
if (item.server == null) {
return true;
}
const settled =
item.clientRequestId == null ? undefined : settledByRequestId.get(item.clientRequestId);
if (settled?.status === 'admitted' || settled?.status === 'cancelled') {
return false;
}
if (settled?.status === 'dead' || settled?.status === 'admitted_pending_boundary') {
return item.clientRequestId == null || !observedClientRequestIds.has(item.clientRequestId);
}
if (item.clientRequestId == null || observedClientRequestIds.has(item.clientRequestId)) {
return false;
}
if (!authoritativeSnapshot) {
return true;
}
return (
item.server.status === 'sending' ||
item.server.status === 'uncertain' ||
item.server.status === 'indeterminate' ||
item.server.status === 'rejected'
);
});
@ -363,12 +476,93 @@ export default function useSteering({
store.activeGenerationProtocolVersionByConvoId(queueKey),
);
const applyQueuedTurnReceipts = useRecoilCallback(
({ snapshot, set }) =>
(receipts: AgentQueuedTurnReceipt[], source: QueuedTurnReceiptSource = 'snapshot') => {
const previousSettled = snapshot
.getLoadable(store.settledQueuedTurnReceiptsByConvoId(queueKey))
.getValue();
const previousPending = snapshot
.getLoadable(store.pendingQueuedTurnEnqueueIdsByConvoId(queueKey))
.getValue();
const completedRequestIds = new Set(
source === 'enqueue' ? receipts.map((receipt) => receipt.clientRequestId) : [],
);
const nextPending = previousPending.filter((id) => !completedRequestIds.has(id));
const settledByRequestId = new Map(
previousSettled.map((receipt) => [receipt.clientRequestId, receipt]),
);
for (const receipt of receipts) {
const existing = settledByRequestId.get(receipt.clientRequestId);
const settled = mergeSettledQueuedTurnEvidence(existing, receipt, source);
if (settled == null) {
continue;
}
settledByRequestId.set(receipt.clientRequestId, settled);
}
const terminalForReconciliation = new Map(settledByRequestId);
const pendingRequestIds = new Set(nextPending);
const nextSettled = [...settledByRequestId.values()].filter(
(receipt) =>
(receipt.status === 'admitted' &&
receipt.rootPredecessor !== true &&
receipt.boundaryConsumed !== true) ||
pendingRequestIds.has(receipt.clientRequestId),
);
if (source === 'enqueue') {
set(store.pendingQueuedTurnEnqueueIdsByConvoId(queueKey), nextPending);
}
if (
nextSettled.length !== previousSettled.length ||
nextSettled.some((receipt, index) => receipt !== previousSettled[index])
) {
set(store.settledQueuedTurnReceiptsByConvoId(queueKey), nextSettled);
}
if (source !== 'direct') {
set(store.queuedMessagesByConvoId(queueKey), (previous) =>
reconcileServerQueuedTurns(
previous,
receipts,
terminalForReconciliation,
source === 'snapshot',
),
);
}
},
[queueKey],
);
const finishQueuedTurnEnqueue = useRecoilCallback(
({ snapshot, set }) =>
(clientRequestId: string): boolean => {
const settledReceipts = snapshot
.getLoadable(store.settledQueuedTurnReceiptsByConvoId(queueKey))
.getValue();
const settled = settledReceipts.find(
(receipt) => receipt.clientRequestId === clientRequestId,
);
set(store.pendingQueuedTurnEnqueueIdsByConvoId(queueKey), (previous) =>
previous.filter((id) => id !== clientRequestId),
);
if (
settled != null &&
(settled.status !== 'admitted' || settled.boundaryConsumed === true)
) {
set(store.settledQueuedTurnReceiptsByConvoId(queueKey), (previous) =>
previous.filter((receipt) => receipt.clientRequestId !== clientRequestId),
);
}
return settled != null;
},
[queueKey],
);
useEffect(() => {
if (!serverQueueEnabled || serverQueuedTurns == null) {
return;
}
setQueuedMessages((previous) => reconcileServerQueuedTurns(previous, serverQueuedTurns));
}, [serverQueueEnabled, serverQueuedTurns, setQueuedMessages]);
applyQueuedTurnReceipts(serverQueuedTurns);
}, [serverQueueEnabled, serverQueuedTurns, applyQueuedTurnReceipts]);
useEffect(() => {
if (!serverQueueEnabled || nextReconciliationExpiry == null) {
return;
@ -796,6 +990,9 @@ export default function useSteering({
if (!serverOwned || clientRequestId == null) {
return;
}
set(store.pendingQueuedTurnEnqueueIdsByConvoId(queueKey), (previous) =>
previous.includes(clientRequestId) ? previous : [...previous, clientRequestId],
);
const serverFiles = toQueuedTurnFileRefs(item.files);
/** Commit the optimistic Recoil row before mutation callbacks can
* reconcile it. This also makes synchronous test/adaptor completions
@ -820,29 +1017,12 @@ export default function useSteering({
},
{
onSuccess: (receipt) => {
updateQueuedMessage(item.id, (current) => {
if (receipt.status === 'admitted' || receipt.status === 'cancelled') {
return null;
}
return {
...current,
text: receipt.text,
createdAt: queuedTurnCreatedAt(receipt),
parentMessageId: receipt.parentMessageId,
server: {
id: receipt.queuedTurnId,
status: receipt.status === 'dead' ? 'rejected' : receipt.status,
revision: receipt.revision,
...(receipt.position != null && { position: receipt.position }),
...(receipt.failure?.code != null && { errorCode: receipt.failure.code }),
...(receipt.failure?.message != null && {
errorMessage: receipt.failure.message,
}),
},
};
});
applyQueuedTurnReceipts([receipt], 'enqueue');
},
onError: (error) => {
if (finishQueuedTurnEnqueue(clientRequestId)) {
return;
}
updateQueuedMessage(item.id, (current) => {
if (isDefiniteQueuedTurnsUnsupported(error)) {
const {
@ -891,6 +1071,8 @@ export default function useSteering({
markQueuedFilesUsage,
activeGenerationCreatedAt,
enqueueAgentQueuedTurn,
applyQueuedTurnReceipts,
finishQueuedTurnEnqueue,
updateQueuedMessage,
],
);
@ -1115,6 +1297,7 @@ export default function useSteering({
});
return false;
}
applyQueuedTurnReceipts([receipt], 'direct');
return downgradeServerQueuedTurn(item.id);
} catch {
showToast({
@ -1159,6 +1342,7 @@ export default function useSteering({
[
cancelSteer,
cancelAgentQueuedTurn,
applyQueuedTurnReceipts,
conversationId,
downgradeQueuedRecovery,
downgradeServerQueuedTurn,

View file

@ -1791,6 +1791,7 @@
"com_ui_queued_messages": "Queued messages",
"com_ui_queued_quote_count": "{{0}} quoted excerpts included with this message",
"com_ui_queued_turn_failed": "Queued message failed",
"com_ui_queued_turn_reconciliation_required": "Awaiting reconciliation",
"com_ui_quote_selections": "{{0}} selections",
"com_ui_quotes_queued": "Quotes added for your next message",
"com_ui_ran_n_agents": "Ran {{0}} agents",

View file

@ -402,7 +402,7 @@ export type QueuedMessage = {
* after an ambiguous POST could submit the same words twice. */
server?: {
id?: string;
status: 'sending' | 'uncertain' | 'rejected' | 'queued' | 'claimed';
status: 'sending' | 'uncertain' | 'indeterminate' | 'rejected' | 'queued' | 'claimed';
errorCode?: string;
errorMessage?: string;
/** Observation time for a transport-ambiguous enqueue. The logical item
@ -459,6 +459,29 @@ const queuedMessagesByConvoId = atomFamily<QueuedMessage[], string>({
default: [],
});
export type SettledQueuedTurnReceipt = {
clientRequestId: string;
status: 'admitted' | 'admitted_pending_boundary' | 'indeterminate' | 'cancelled' | 'dead';
effectivePredecessorCreatedAt?: number;
rootPredecessor?: true;
boundaryConsumed?: boolean;
};
/** Monotonic client knowledge of terminal server queue receipts. Admission
* records preserve boundary multiplicity by request identity. Other terminal
* records exist only while their original enqueue callback is outstanding. */
const settledQueuedTurnReceiptsByConvoId = atomFamily<SettledQueuedTurnReceipt[], string>({
key: 'settledQueuedTurnReceiptsByConvoId',
default: [],
});
/** Enqueue callbacks that can still race newer GET/cancellation evidence.
* Entries retire as soon as that one callback settles. */
const pendingQueuedTurnEnqueueIdsByConvoId = atomFamily<string[], string>({
key: 'pendingQueuedTurnEnqueueIdsByConvoId',
default: [],
});
/**
* One-shot run-termination signal written by the SSE final/error handlers and
* consumed (reset to null) by `useQueueDrain`. Keyed by chat index like
@ -788,6 +811,8 @@ export default {
pendingQuotesByConvoId,
pendingSteersByConvoId,
queuedMessagesByConvoId,
settledQueuedTurnReceiptsByConvoId,
pendingQueuedTurnEnqueueIdsByConvoId,
runEndByIndex,
pendingRunEndByConvoId,
drainAfterAbortByIndex,

View file

@ -67,10 +67,21 @@ describe('Agent queued-turn HTTP admission receipts', () => {
const enqueueAgentQueuedTurn = jest
.fn()
.mockResolvedValueOnce({ turn: turn('queued'), replayed: false });
const admitted = {
...turn('admitted'),
terminalReceipt: {
outcome: 'admitted' as const,
settledAt: new Date('2026-08-30T12:01:00Z'),
admissionId: 'client-request-1',
generationId: 'generation-1',
generationCreatedAt: 43,
effectivePredecessorCreatedAt: 42,
},
};
const getAgentQueuedTurnByClientRequestId = jest
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(turn('admitted'));
.mockResolvedValueOnce(admitted);
const schedule = jest.fn().mockRejectedValueOnce(new Error('scheduler unavailable'));
const methods = {
getConvo: jest.fn(async () => ({
@ -104,6 +115,7 @@ describe('Agent queued-turn HTTP admission receipts', () => {
queuedTurnId: 'queued-turn-1',
clientRequestId: 'client-request-1',
status: 'admitted',
effectivePredecessorCreatedAt: 42,
},
},
});
@ -172,6 +184,47 @@ describe('Agent queued-turn HTTP admission receipts', () => {
expect(cancel).toHaveBeenCalledWith(expect.objectContaining({ queuedTurnId: 'queued-turn-1' }));
});
it('projects an explicit root admission without a timestamp boundary', async () => {
const admitted = {
...turn('admitted'),
terminalReceipt: {
outcome: 'admitted' as const,
settledAt: new Date('2026-08-30T12:01:00Z'),
admissionId: 'client-request-1',
generationId: 'generation-root',
generationCreatedAt: 43,
lineagePredecessorId: 'root:message-identity',
rootPredecessor: true as const,
},
};
const methods = {
getConvo: jest.fn(async () => ({ agent_id: 'agent_1', endpoint: 'agents' })),
listAgentQueuedTurnReceipts: jest.fn(async () => [admitted]),
};
const deps = {
methods: methods as unknown as AgentQueuedTurnMethods & {
getConvo: typeof methods.getConvo;
},
lifecycle: { schedule: jest.fn(), cancel: jest.fn() },
checkAgentAccess: jest.fn(async () => true),
} satisfies AgentQueuedTurnHttpDeps;
await expect(
handleAgentQueuedTurnList({ id: USER_ID }, 'conversation-1', deps),
).resolves.toMatchObject({
status: 200,
body: {
queuedTurns: [
{
queuedTurnId: 'queued-turn-1',
status: 'admitted',
rootPredecessor: true,
},
],
},
});
});
it('retires a cancelled source after its published delivery receipt expires', async () => {
const cancelled = {
...turn('cancelled'),

View file

@ -90,6 +90,12 @@ function receipt(
...(turn.expectedPredecessorCreatedAt != null && {
expectedPredecessorCreatedAt: turn.expectedPredecessorCreatedAt,
}),
...(turn.terminalReceipt?.outcome === 'admitted' &&
turn.terminalReceipt.effectivePredecessorCreatedAt != null && {
effectivePredecessorCreatedAt: turn.terminalReceipt.effectivePredecessorCreatedAt,
}),
...(turn.terminalReceipt?.outcome === 'admitted' &&
turn.terminalReceipt.rootPredecessor === true && { rootPredecessor: true as const }),
status: turn.status,
...(position != null && { position }),
revision: turn.sequence,

View file

@ -51,6 +51,7 @@ function claim(): AgentQueuedTurnClaim {
clientRequestId: 'client-1',
fingerprint: 'fingerprint-1',
sequence: 1,
admissionSlot: true,
status: 'claimed',
priority: false,
text: 'queued words',
@ -107,7 +108,7 @@ function resolverMethods() {
getEffectiveAgentQueuedTurnPredecessor: jest.fn(
async (
..._args: Parameters<AgentQueuedTurnMethods['getEffectiveAgentQueuedTurnPredecessor']>
): Promise<number | undefined> => undefined,
) => null,
),
markAgentQueuedTurnAdmitted: jest.fn(async () => ({
outcome: 'admitted' as const,
@ -306,6 +307,7 @@ describe('Agent queued-turn continuation', () => {
sourceId: 'queued-turn-1',
claimId: 'trigger-1',
claimBy: 'worker-1',
effectivePredecessorCreatedAt: NOW,
},
{
userId: USER_ID,
@ -325,6 +327,7 @@ describe('Agent queued-turn continuation', () => {
admissionId: 'trigger-1',
generationId: 'conversation-1',
generationCreatedAt: NOW + 1,
effectivePredecessorCreatedAt: NOW,
}),
);
});
@ -342,6 +345,7 @@ describe('Agent queued-turn continuation', () => {
sourceId: 'queued-turn-1',
claimId: 'trigger-1',
claimBy: 'worker-1',
effectivePredecessorCreatedAt: NOW,
};
const admission = {
userId: USER_ID,
@ -361,6 +365,7 @@ describe('Agent queued-turn continuation', () => {
admissionId: 'trigger-1',
generationId: 'conversation-1',
generationCreatedAt: NOW + 1,
effectivePredecessorCreatedAt: NOW,
});
spies.hasAgentQueuedTurnAdmissionReceipt.mockResolvedValueOnce(false);
@ -451,7 +456,10 @@ describe('Agent queued-turn continuation', () => {
const claimed = claim();
claimed.expectedPredecessorCreatedAt = NOW;
spies.claimNextAgentQueuedTurn.mockResolvedValueOnce({ outcome: 'acquired', claim: claimed });
spies.getEffectiveAgentQueuedTurnPredecessor.mockResolvedValueOnce(NOW + 250);
spies.beginAgentQueuedTurnAdmission.mockResolvedValueOnce({
outcome: 'started',
turn: { ...claimed, admissionEffectivePredecessorCreatedAt: NOW + 250 },
});
const resolve = createAgentQueuedTurnResolver({
methods,
getGenerationJob: async () => null,
@ -459,17 +467,53 @@ describe('Agent queued-turn continuation', () => {
claimBy: 'worker-1',
});
await expect(resolve(envelope(), { idempotencyKey: 'trigger-1' })).resolves.toMatchObject({
const prepared = await resolve(envelope(), { idempotencyKey: 'trigger-1' });
expect(prepared).toMatchObject({
status: 'ready',
expectedPredecessorCreatedAt: NOW + 250,
admissionSource: { effectivePredecessorCreatedAt: NOW + 250 },
});
expect(spies.getEffectiveAgentQueuedTurnPredecessor).toHaveBeenCalledWith({
user: new Types.ObjectId(USER_ID),
tenantId: 'tenant-1',
expect(spies.beginAgentQueuedTurnAdmission).toHaveBeenCalledTimes(1);
if (prepared?.status !== 'ready') {
throw new Error('Expected a ready queued turn');
}
await prepared.settleOnAdmission?.({
mode: 'continue',
status: 'started',
conversationId: 'conversation-1',
sequence: 1,
expectedPredecessorCreatedAt: NOW,
streamId: 'stream-2',
generationCreatedAt: NOW + 500,
});
expect(spies.markAgentQueuedTurnAdmitted).toHaveBeenCalledWith(
expect.objectContaining({
queuedTurnId: 'queued-turn-1',
effectivePredecessorCreatedAt: NOW + 250,
}),
);
});
it('dead-letters a legacy admission order that cannot be reconstructed safely', async () => {
const { methods, spies } = resolverMethods();
(
spies.beginAgentQueuedTurnAdmission as unknown as jest.MockedFunction<
AgentQueuedTurnMethods['beginAgentQueuedTurnAdmission']
>
).mockResolvedValueOnce({
outcome: 'order_unavailable',
turn: { ...claim(), status: 'dead' },
});
const resolve = createAgentQueuedTurnResolver({
methods,
getGenerationJob: async () => null,
now: () => NOW,
claimBy: 'worker-1',
});
await expect(resolve(envelope(), { idempotencyKey: 'trigger-1' })).resolves.toEqual({
status: 'settled',
});
expect(spies.releaseAgentQueuedTurn).not.toHaveBeenCalled();
expect(spies.beginAgentQueuedTurnAdmission).toHaveBeenCalledTimes(1);
});
it.each([
@ -776,10 +820,10 @@ describe('Agent queued-turn delivery scheduling', () => {
);
});
it('reconciles quarantined admission receipts during durable recovery', async () => {
it('reconciles a claimed indeterminate legacy admission during durable recovery', async () => {
const turn = {
...queuedTurn('queued-turn-indeterminate', 1),
status: 'dead' as const,
status: 'claimed' as const,
deliveryKey: 'delivery-indeterminate',
deliveryState: 'published' as const,
admissionId: 'delivery-indeterminate',
@ -841,7 +885,7 @@ describe('Agent queued-turn delivery scheduling', () => {
it('rotates source-fenced ambiguity without trusting transient job-store evidence', async () => {
const turn = {
...queuedTurn('queued-turn-source-fenced', 1),
status: 'dead' as const,
status: 'claimed' as const,
deliveryKey: 'delivery-source-fenced',
deliveryState: 'published' as const,
admissionId: 'delivery-source-fenced',

View file

@ -153,6 +153,9 @@ function parseQueuedTurnAdmissionSource(raw: unknown): AgentContinuationAdmissio
const sourceId = 'sourceId' in raw ? raw.sourceId : undefined;
const claimId = 'claimId' in raw ? raw.claimId : undefined;
const claimBy = 'claimBy' in raw ? raw.claimBy : undefined;
const effectivePredecessorCreatedAt =
'effectivePredecessorCreatedAt' in raw ? raw.effectivePredecessorCreatedAt : undefined;
const lineagePredecessorId = 'lineagePredecessorId' in raw ? raw.lineagePredecessorId : undefined;
if (
typeof sourceId !== 'string' ||
sourceId.length === 0 ||
@ -162,11 +165,28 @@ function parseQueuedTurnAdmissionSource(raw: unknown): AgentContinuationAdmissio
claimId.length > 128 ||
typeof claimBy !== 'string' ||
claimBy.length === 0 ||
claimBy.length > 256
claimBy.length > 256 ||
(effectivePredecessorCreatedAt != null &&
(typeof effectivePredecessorCreatedAt !== 'number' ||
!Number.isSafeInteger(effectivePredecessorCreatedAt) ||
effectivePredecessorCreatedAt < 0)) ||
(lineagePredecessorId != null &&
(typeof lineagePredecessorId !== 'string' ||
lineagePredecessorId.length === 0 ||
lineagePredecessorId.length > 128))
) {
throw new TypeError('Agent queued turn admission source is invalid');
}
return { source: AGENT_QUEUED_TURN_SOURCE, sourceId, claimId, claimBy };
return {
source: AGENT_QUEUED_TURN_SOURCE,
sourceId,
claimId,
claimBy,
...(typeof effectivePredecessorCreatedAt === 'number' && {
effectivePredecessorCreatedAt,
}),
...(typeof lineagePredecessorId === 'string' && { lineagePredecessorId }),
};
}
/** Commits the source-owned admission receipt only after the controller has
@ -195,6 +215,12 @@ async function settleAgentQueuedTurnExecutionAdmission(
admissionMode: 'ordinary',
generationId: input.generationId,
generationCreatedAt: input.generationCreatedAt,
...(source.effectivePredecessorCreatedAt != null && {
effectivePredecessorCreatedAt: source.effectivePredecessorCreatedAt,
}),
...(source.lineagePredecessorId != null && {
lineagePredecessorId: source.lineagePredecessorId,
}),
settledAt: new Date(),
});
if (settled.outcome === 'conflict') {
@ -226,6 +252,12 @@ async function verifyAgentQueuedTurnExecutionAdmission(
admissionId: input.clientRequestId,
generationId: input.generationId,
generationCreatedAt: input.generationCreatedAt,
...(source.effectivePredecessorCreatedAt != null && {
effectivePredecessorCreatedAt: source.effectivePredecessorCreatedAt,
}),
...(source.lineagePredecessorId != null && {
lineagePredecessorId: source.lineagePredecessorId,
}),
});
if (!confirmed) {
throw new Error('The queued turn execution admission is not yet confirmed');
@ -578,17 +610,6 @@ function createAgentQueuedTurnResolver({
return { status: 'settled' };
}
const effectivePredecessorCreatedAt =
(await methods.getEffectiveAgentQueuedTurnPredecessor({
user: claim.user,
...(claim.tenantId != null && { tenantId: claim.tenantId }),
conversationId: claim.conversationId,
sequence: claim.sequence,
...(claim.expectedPredecessorCreatedAt != null && {
expectedPredecessorCreatedAt: claim.expectedPredecessorCreatedAt,
}),
})) ?? claim.expectedPredecessorCreatedAt;
const admission = await methods.beginAgentQueuedTurnAdmission({
user: claim.user,
...(claim.tenantId != null && { tenantId: claim.tenantId }),
@ -607,9 +628,11 @@ function createAgentQueuedTurnResolver({
deferWithoutAttempt: true,
});
}
if (admission.outcome === 'retired') {
if (admission.outcome === 'retired' || admission.outcome === 'order_unavailable') {
return { status: 'settled' };
}
const effectivePredecessorCreatedAt = admission.turn.admissionEffectivePredecessorCreatedAt;
const lineagePredecessorId = admission.turn.admissionLineagePredecessorId;
return {
status: 'ready',
@ -626,6 +649,8 @@ function createAgentQueuedTurnResolver({
sourceId: claim.queuedTurnId,
claimId: claim.claimId,
claimBy: claim.claimBy,
...(effectivePredecessorCreatedAt != null && { effectivePredecessorCreatedAt }),
...(lineagePredecessorId != null && { lineagePredecessorId }),
},
releaseOnDefiniteFailure: async (error) => {
if (
@ -659,6 +684,8 @@ function createAgentQueuedTurnResolver({
...(result.generationCreatedAt != null && {
generationCreatedAt: result.generationCreatedAt,
}),
...(effectivePredecessorCreatedAt != null && { effectivePredecessorCreatedAt }),
...(lineagePredecessorId != null && { lineagePredecessorId }),
settledAt: new Date(now()),
});
if (settled.outcome === 'conflict') {
@ -794,7 +821,10 @@ function createAgentQueuedTurnScheduler({
}),
);
try {
if (turn.status === 'claimed') {
const isIndeterminate =
turn.terminalReceipt?.outcome === 'dead' &&
turn.terminalReceipt.failure?.code === 'ADMISSION_INDETERMINATE';
if (turn.status === 'claimed' && !isIndeterminate) {
const result = await runAsSystem(() =>
methods.deadLetterAgentQueuedTurn({
user: turn.user,

View file

@ -43,6 +43,8 @@ export interface AgentContinuationAdmissionSource {
sourceId: string;
claimId: string;
claimBy: string;
effectivePredecessorCreatedAt?: number;
lineagePredecessorId?: string;
}
export type AgentTriggerContinuePreparation =

View file

@ -94,6 +94,34 @@ describe('agent queued turn schemas', () => {
expect(agentQueuedTurnReceiptSchema.parse(receipt)).toEqual(receipt);
});
it('parses the effective boundary consumed by an admitted turn', () => {
const receipt = {
...request,
queuedTurnId: 'turn-1',
status: 'admitted',
effectivePredecessorCreatedAt: 84,
revision: 2,
createdAt: '2026-08-30T12:00:00.000Z',
updatedAt: '2026-08-30T12:01:00.000Z',
};
expect(agentQueuedTurnReceiptSchema.parse(receipt)).toEqual(receipt);
});
it('parses an explicit root admission without a synthetic timestamp', () => {
const receipt = {
...request,
queuedTurnId: 'turn-root',
status: 'admitted',
rootPredecessor: true,
revision: 2,
createdAt: '2026-08-30T12:00:00.000Z',
updatedAt: '2026-08-30T12:01:00.000Z',
};
expect(agentQueuedTurnReceiptSchema.parse(receipt)).toEqual(receipt);
});
it('requires durability only when the capability is supported', () => {
expect(agentQueuedTurnCapabilitySchema.parse({ supported: false })).toEqual({
supported: false,

View file

@ -52,7 +52,13 @@ export type TCancelAgentQueuedTurnRequest = z.infer<typeof cancelAgentQueuedTurn
export const agentQueuedTurnReceiptSchema = enqueueAgentQueuedTurnSchema.extend({
queuedTurnId: z.string().trim().min(1),
status: z.enum(agentQueuedTurnStatuses),
/** Effective generation boundary consumed by an admitted turn. This can
* advance beyond the originally captured root as queued turns chain. */
effectivePredecessorCreatedAt: z.number().int().nonnegative().optional(),
/** Explicitly proves that this admission consumed no predecessor boundary. */
rootPredecessor: z.literal(true).optional(),
position: z.number().int().nonnegative().optional(),
/** Immutable queue sequence retained as `revision` for wire compatibility. */
revision: z.number().int().nonnegative(),
failure: z
.object({

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,9 @@ const terminalReceiptSchema = new Schema(
admissionMode: { type: String, enum: ['warm', 'ordinary'] },
generationId: { type: String, maxlength: 256 },
generationCreatedAt: { type: Number, min: 0 },
effectivePredecessorCreatedAt: { type: Number, min: 0 },
lineagePredecessorId: { type: String, maxlength: 128 },
rootPredecessor: { type: Boolean, enum: [true] },
failure: { type: failureSchema },
},
{ _id: false },
@ -57,6 +60,7 @@ const queuedTurnSchema: Schema<IAgentQueuedTurnDocument> = new Schema(
sequence: { type: Number, min: 1 },
reservationWriterId: { type: String, maxlength: 128 },
activeSlot: { type: Number, min: 0, max: 99 },
admissionSlot: { type: Boolean },
status: {
type: String,
enum: ['reserving', 'queued', 'claimed', 'admitted', 'cancelled', 'dead'],
@ -84,6 +88,8 @@ const queuedTurnSchema: Schema<IAgentQueuedTurnDocument> = new Schema(
claimUntil: { type: Date },
admissionStartedAt: { type: Date },
admissionId: { type: String, maxlength: 128 },
admissionEffectivePredecessorCreatedAt: { type: Number, min: 0 },
admissionLineagePredecessorId: { type: String, maxlength: 128 },
admissionProtocolVersion: { type: Number, enum: [2] },
reconciliationAvailableAt: { type: Date },
reconciliationClaimId: { type: String, maxlength: 128 },
@ -115,6 +121,33 @@ queuedTurnSchema.index(
partialFilterExpression: { activeSlot: { $exists: true } },
},
);
queuedTurnSchema.index(
{ tenantId: 1, user: 1, conversationId: 1, laneId: 1, status: 1 },
{
name: 'agent_queued_turn_claim_lane',
unique: true,
partialFilterExpression: { status: 'claimed' },
},
);
queuedTurnSchema.index(
{ tenantId: 1, user: 1, conversationId: 1, laneId: 1 },
{
name: 'agent_queued_turn_admission_started_lane',
unique: true,
/** `admissionStartedAt` is written by both legacy and current workers and
* survives legacy `dead/ADMISSION_INDETERMINATE` quarantine. This is the
* cross-version fence after a claim crosses the provider boundary. */
partialFilterExpression: { admissionStartedAt: { $exists: true } },
},
);
queuedTurnSchema.index(
{ tenantId: 1, user: 1, conversationId: 1, laneId: 1, admissionSlot: 1 },
{
name: 'agent_queued_turn_admission_slot',
unique: true,
partialFilterExpression: { admissionSlot: true },
},
);
queuedTurnSchema.index(
{
tenantId: 1,

View file

@ -37,6 +37,12 @@ export interface AgentQueuedTurnTerminalReceipt {
admissionMode?: 'warm' | 'ordinary';
generationId?: string;
generationCreatedAt?: number;
/** Effective predecessor consumed by this admission after queued-turn chaining. */
effectivePredecessorCreatedAt?: number;
/** Durable lineage node: the root-message identity or predecessor queued-turn identity. */
lineagePredecessorId?: string;
/** This admission consumed no predecessor generation boundary. */
rootPredecessor?: true;
failure?: AgentQueuedTurnFailure;
}
@ -57,6 +63,8 @@ export interface IAgentQueuedTurn {
reservationWriterId?: string;
/** Bounded active-lane capacity token. Present only while queued/claimed. */
activeSlot?: number;
/** Unique durable admission-order reservation for one conversation lane. */
admissionSlot?: boolean;
status: AgentQueuedTurnStatus;
priority: boolean;
text: string;
@ -75,6 +83,10 @@ export interface IAgentQueuedTurn {
/** Durable proof that ordinary admission may have crossed the HTTP boundary. */
admissionStartedAt?: Date;
admissionId?: string;
/** Effective predecessor durably fenced before provider admission begins. */
admissionEffectivePredecessorCreatedAt?: number;
/** Durable lineage node fenced with the effective predecessor. */
admissionLineagePredecessorId?: string;
/** Version 2 requires accepted and deduplicated execution responses to prove
* the exact post-invocation source receipt. */
admissionProtocolVersion?: 2;
@ -148,6 +160,7 @@ export type AgentQueuedTurnActiveRecord = Pick<
export interface AgentQueuedTurnClaim extends AgentQueuedTurnRecord {
status: 'claimed';
admissionSlot: true;
claimId: string;
claimBy: string;
claimUntil: Date;