mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
fix(chat): preserve inbound notice cursor precision
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7bc938d7a3
commit
d6dfbb495d
3 files changed
+52
-5
No files matched your search
@@ -71126,6 +71126,37 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
};
|
||||
}
|
||||
|
||||
it("advances pre-run FIFO selection past a suppressed page with microsecond timestamps", async () => {
|
||||
const context = await deferredChatQueueFixture();
|
||||
const rejectedIds = Array.from({ length: 50 }, () => randomUUID());
|
||||
try {
|
||||
const [receipt] = await readAgentWakeupRequestsById(context.action.id);
|
||||
const before = new Date(context.action.createdAt.getTime() - 1000).toISOString();
|
||||
await db.insert(chatActions).values(rejectedIds.map(id => ({
|
||||
...context.action, id, providerActionId: `inbound_wakeup:${id}`,
|
||||
payload: { ...context.action.payload, commentId: randomUUID() },
|
||||
createdAt: sql`${before}::timestamptz + interval '0.000123 seconds'`,
|
||||
})));
|
||||
await db.insert(agentWakeupRequests).values(rejectedIds.map(id => ({
|
||||
...receipt, id, idempotencyKey: `chat-cursor-fixture:${id}`,
|
||||
})));
|
||||
// Rejected current authority must not keep the same full page at the
|
||||
// head of every bounded pass and starve the later admitted message.
|
||||
const first = await context.service.enqueueInboundWakeupPublications(50);
|
||||
const second = await context.service.enqueueInboundWakeupPublications(50);
|
||||
expect(first + second).toBe(1);
|
||||
const rows = await readChatPublicationsByEndpointId(context.endpoint.id);
|
||||
expect(rows).toEqual([expect.objectContaining({
|
||||
commentId: context.action.payload.commentId,
|
||||
payload: { text: "Your follow-up is queued.", progressState: "queued" },
|
||||
})]);
|
||||
} finally {
|
||||
await context.service.shutdown();
|
||||
await db.delete(chatActions).where(inArray(chatActions.id, [...rejectedIds, context.action.id]));
|
||||
await db.delete(agentWakeupRequests).where(inArray(agentWakeupRequests.id, [...rejectedIds, context.action.id]));
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps one durable pre-run FIFO notice across coalescing and restart, then reuses only its successor lane", async () => {
|
||||
const context = await deferredChatQueueFixture();
|
||||
let restarted: ChatChannelService | null = null;
|
||||
|
||||
@@ -20,7 +20,7 @@ type NoticeOptions = {
|
||||
};
|
||||
export function createInboundWakeNotices(db: Db, options: NoticeOptions) {
|
||||
const { authorizeInboundWakeup, enqueueFailedChatRetryPublications, authorizeRetainedChatSourcePublication, isExternalActionAuthorizationChange } = options;
|
||||
let inboundWakeNoticeCursor: { createdAt: Date; id: string } | null = null;
|
||||
let inboundWakeNoticeCursor: { createdAt: string; id: string } | null = null;
|
||||
async function currentInboundWakeNotice(
|
||||
tx: DbOrTransaction,
|
||||
action: typeof chatActions.$inferSelect,
|
||||
@@ -162,7 +162,7 @@ export function createInboundWakeNotices(db: Db, options: NoticeOptions) {
|
||||
// A bounded keyset sweep, not an in-memory work queue. Advancing even past
|
||||
// revoked candidates prevents an inaccessible old message starving others.
|
||||
const candidates = await db
|
||||
.select({ action: chatActions })
|
||||
.select({ action: chatActions, cursorCreatedAt: sql<string>`${chatActions.createdAt}::text` })
|
||||
.from(chatActions)
|
||||
.innerJoin(
|
||||
agentWakeupRequests,
|
||||
@@ -193,7 +193,7 @@ export function createInboundWakeNotices(db: Db, options: NoticeOptions) {
|
||||
and ${agentWakeupRequests.status} not in ('cancelled','failed','skipped') and not (${sourceRemoved}))
|
||||
or ${hasVisibleQueue})`,
|
||||
cursor
|
||||
? sql`(${chatActions.createdAt}, ${chatActions.id}) > (${cursor.createdAt.toISOString()}::timestamptz, ${cursor.id}::uuid)`
|
||||
? sql`(${chatActions.createdAt}, ${chatActions.id}) > (${cursor.createdAt}::timestamptz, ${cursor.id}::uuid)`
|
||||
: undefined,
|
||||
sql`not exists (select 1 from chat_publications notice
|
||||
where notice.company_id = ${chatActions.companyId}
|
||||
@@ -206,10 +206,10 @@ export function createInboundWakeNotices(db: Db, options: NoticeOptions) {
|
||||
)
|
||||
.orderBy(asc(chatActions.createdAt), asc(chatActions.id))
|
||||
.limit(Math.max(1, Math.min(limit, 200)));
|
||||
const last = candidates.at(-1)?.action;
|
||||
const last = candidates.at(-1);
|
||||
inboundWakeNoticeCursor =
|
||||
candidates.length >= Math.max(1, Math.min(limit, 200)) && last
|
||||
? { createdAt: last.createdAt, id: last.id }
|
||||
? { createdAt: last.cursorCreatedAt, id: last.action.id }
|
||||
: null;
|
||||
let inserted = retryInserted;
|
||||
for (const { action } of candidates) {
|
||||
|
||||
Reference in new issue
Block a user