fix(server): preserve Discord backlog and queued scopes

Retain the real company-deletion regression pending its prerequisite dependency-order repair.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-25 01:38:33 -05:00
1 parent 543f9acf01
commit 8df31d46c0
4 files changed
+118 -3

No files matched your search

@@ -1119,3 +1119,16 @@ batching cases pass; server types pass. Propagate the parent and apply the same
bounded continuation to Discord, with its own regression. The Discord review also
requests preserving queued scopes after malformed hints and broader producer
coverage. Its initial head has Greptile 5/5 but these findings remain open.
Discord review repairs are in progress. Both new worker regressions failed before
repair: the 300-command backlog did not drain promptly, and an unknown hint lost
later scopes. Bounded backlog continuations and full reconstruction after malformed
scope parsing repair them; all 62 combined command/health/batching cases pass.
Real credential rotation also passes. The company-deletion producer test exposed
an existing dependency-order failure: agents are deleted before chat endpoints
whose assigned-agent foreign key is RESTRICT. Configuring the fixture lifecycle
removed the initial missing-worker failure but reproduced this actual SQL failure.
Keep the failing producer test; repair the deletion dependency in its own stack
PR before publishing the Discord follow-up. Current local changes are not final
acceptance. The original full inventory and remaining CI gates are still open.
@@ -1,3 +1,5 @@
import { configureAgentLifecycle } from "../services/agent-lifecycle.js";
import { deleteCompany } from "../services/company-deletion.js";
import { idleWorkSnapshot } from "../services/task-admission.js";
import { readFreshWorkspaceReadiness } from "../services/fresh-workspace-continuation.js";
import { requestTaskRetry } from "../services/task-retry.js";
@@ -6781,6 +6783,60 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
}
});
it.each(["credential rotation", "company deletion"] as const)("updates scheduled registration after real %s", async (mutation) => {
const f = await registrationFixture();
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const onError = vi.fn();
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError });
let deleted = false;
let lifecycle: ReturnType<typeof configureAgentLifecycle> | undefined;
try {
await f.service.registerCommandWork(coordinator, () => true).ready;
await f.configure();
await vi.advanceTimersByTimeAsync(31_000);
await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBeGreaterThan(Date.now()));
const before = coordinator.nextWakeAt()!;
const owners = await db.select().from(chatDiscordCommandOwners)
.where(eq(chatDiscordCommandOwners.endpointId, f.endpoint.id));
if (mutation === "credential rotation") {
await f.service.configure(f.endpoint.id, {
action: "reconnect", credentials: { ...f.credentials, botToken: "discord-rotated-secret" },
}, "owner-user");
const [saved] = await db.select().from(chatActions).where(and(
eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "discord_command_registration"),
));
const expected = Date.parse(String(saved.result!.retryAt));
expect(expected).toBeGreaterThan(before);
await vi.advanceTimersByTimeAsync(31_000);
await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBe(expected));
expect(f.calls).toEqual(["GET", "POST", "GET"]);
} else {
lifecycle = configureAgentLifecycle(db, {
requiredPluginIds: async () => [], runHost: async () => "complete", runPlugin: async () => "complete",
});
await deleteCompany(db, f.fixture.companyId);
deleted = true;
await vi.advanceTimersByTimeAsync(31_000);
await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBeNull());
expect(await db.select().from(chatDiscordCommandOwners)
.where(eq(chatDiscordCommandOwners.endpointId, f.endpoint.id))).toEqual(owners);
await f.service.shutdown();
const reads = vi.spyOn(db, "select"), writes = vi.spyOn(db, "transaction");
try {
await vi.advanceTimersByTimeAsync(120_000);
expect(reads).not.toHaveBeenCalled(); expect(writes).not.toHaveBeenCalled();
} finally { reads.mockRestore(); writes.mockRestore(); }
expect(f.calls).toEqual(["GET", "POST"]);
}
expect(onError).not.toHaveBeenCalled();
} finally {
await lifecycle?.stop();
await coordinator.stop(); vi.useRealTimers();
if (deleted) await f.service.shutdown();
else await retireRegistrationFixture(f.service, f.endpoint.id);
}
});
it("keeps mention setup usable on registration failure and upgrades automatically when due", async () => {
const f = await registrationFixture("unavailable");
try {
@@ -54,8 +54,9 @@ export function registerDiscordCommandWork(
return;
}
const hint = requested[index];
const scope = decodeScope(hint);
let scope: CommandRegistrationScope | undefined;
try {
scope = decodeScope(hint);
const rows = await service.load(scope, signal);
for (const entry of deadlines.values()) if (matches(entry, scope)) deadlines.delete(entry.id);
for (const entry of rows) deadlines.set(entry.id, entry);
@@ -63,7 +64,7 @@ export function registerDiscordCommandWork(
const entry = deadlines.get(id);
if (!entry || entry.dueAt > Date.now()) failures.delete(id);
}
} catch (error) { dirty.add(hint); unavailable.push(scope); errors.push(error); }
} catch (error) { dirty.add(scope ? hint : "*"); unavailable.push(scope); errors.push(error); }
}
const due = [...deadlines.values()]
.filter(entry => !unavailable.some(scope => matches(entry, scope)) &&
@@ -84,6 +85,7 @@ export function registerDiscordCommandWork(
errors.push(error);
}
}
if (due.length > batch.entries.length) earliest = Date.now() + 1_000;
if (errors.length) throw new AggregateError(errors, "Discord command registration could not settle");
},
hasPending: async () => dirty.size > 0,
@@ -1,5 +1,5 @@
import { reconcileDatabaseWork } from "@paperclipai/db";
import { DELIVERY_QUEUES } from "./delivery-work-notifications.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
import { discordCommandSchedule } from "./chat-channels/discord/command-schedule.js";
import { registerDiscordCommandWork } from "./chat-channels/discord/command-work.js";
import { notifyDiscordCommandRegistration } from "./chat-channels/discord/command-work-notifications.js";
@@ -841,4 +841,48 @@ suite("Discord command ownership store (real PostgreSQL, no network)", () => {
expect(errors).toEqual([]);
});
it("drains a large due backlog without a thirty-second delay between bounded batches", async () => {
await reconcileDatabaseWork(db, DELIVERY_QUEUES.chatCommandRegistration);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const entries = Array.from({ length: 300 }, (_, index) => ({
id: `command-${index}`, companyId: `company-${index % 10}`, connectionId: `connection-${index}`, dueAt: 0,
}));
const check = vi.fn(async (id: string) => ({ ...entries.find(entry => entry.id === id)!, dueAt: Date.now() + 300_000 }));
const errors: unknown[] = [];
coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError: error => errors.push(error) });
await registerDiscordCommandWork(coordinator, { load: async () => entries, check }, () => true).ready;
expect(check).toHaveBeenCalledTimes(25);
await vi.advanceTimersByTimeAsync(15_000);
expect(check).toHaveBeenCalledTimes(300);
expect(new Set(check.mock.calls.map(([id]) => id)).size).toBe(300);
await sqlQuiet(120_000);
expect(check).toHaveBeenCalledTimes(300);
expect(errors).toEqual([]);
});
it("recovers queued scopes after an unknown hint instead of dropping the wake", async () => {
await reconcileDatabaseWork(db, DELIVERY_QUEUES.chatCommandRegistration);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
const entry = { id: "endpoint", companyId: "company", connectionId: "connection", dueAt: 0 };
let owed = false;
const check = vi.fn(async () => { owed = false; return null; });
const errors: unknown[] = [];
coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError: error => errors.push(error) });
await registerDiscordCommandWork(coordinator, { load: async () => owed ? [entry] : [], check }, () => true).ready;
await db.transaction(async tx => {
await notifyDeliveryWork(tx, DELIVERY_QUEUES.chatCommandRegistration, "unknown:legacy");
await notifyDiscordCommandRegistration(tx, { kind: "endpoint", id: entry.id });
owed = true;
});
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1_000);
expect(errors).toHaveLength(1);
}, { timeout: 5_000 });
await vi.advanceTimersByTimeAsync(31_000);
await vi.waitFor(() => expect(check).toHaveBeenCalledTimes(1));
await vi.waitFor(() => expect(coordinator!.nextWakeAt()).toBeNull());
await sqlQuiet(120_000);
expect(errors).toHaveLength(1);
});
});