Chat-wake tasks live and die with their reply run
Of the 202 tasks stranded in the backlog on July 9, 200 were "Reply to ..." wake vehicles: every agent @mention creates a task plus a queued run, the reply goes out, and nothing ever closed the task — a task with a completed run is never enqueued again, so each one became a permanent zombie row. On top of that, every message in an active thread minted a fresh wake even when the target already had one queued, multiplying runs that would each read the same conversation. The lifecycle contract, made explicit: - The reply run IS the wake task's deliverable. Completing a chat_mention run now closes its task (backlog/in_progress -> done, via the audited status path). Tasks an agent moved to review are left alone. - One pending wake per (agent, thread): wakeFromMention returns the queued run instead of minting a duplicate — that run reads the thread's newest messages when it executes anyway. - Migration 054 heals existing rows: wake tasks whose runs are all spent close as done; in-flight wakes keep working. This also stops Fix 1's retry-budget healing from resurrecting 31 stale replies on next launch. The chat-governance loop test now completes each pending wake run between posts, matching how the exchange actually interleaves in production.
This commit is contained in:
1 parent
927bdc91f2
commit
2526633bfb
9 files changed
+149
-4
No files matched your search
@@ -98,6 +98,12 @@ describe("inter-agent reply", () => {
|
||||
// Execute Dana's woken run: she replies in the CEO's thread.
|
||||
await taskExecution.executeRun(orgId, wake!.id);
|
||||
|
||||
// The reply was the wake task's whole deliverable, so completing the run closes it.
|
||||
const wakeTask = connection.db.prepare(
|
||||
"select t.status from tasks t join task_runs r on r.task_id = t.id where r.id = ?",
|
||||
).get(wake!.id) as { status: string };
|
||||
expect(wakeTask).toEqual({ status: "done" });
|
||||
|
||||
const engineering = connection.db.prepare("select id from chat_spaces where org_id = ? and name = 'engineering'").get(orgId) as { id: string };
|
||||
const messages = snapshot.load().chatMessages.filter((m) => m.orgId === orgId && m.spaceId === engineering.id);
|
||||
const senders = new Set(messages.map((m) => m.senderId));
|
||||
|
||||
@@ -59,5 +59,26 @@ describe("thread participant wake", () => {
|
||||
const runsAfterReply = connection.db.prepare("select count(*) as c from task_runs where org_id = ? and trigger = 'chat_mention'")
|
||||
.get(orgId) as { c: number };
|
||||
expect(runsAfterReply.c).toBeGreaterThan(runsBeforeReply.c);
|
||||
|
||||
// A second human message in the same thread coalesces into the still-queued
|
||||
// wake instead of spending another run on the same conversation.
|
||||
connection.transaction(() => {
|
||||
chat.sendMessage({
|
||||
orgId,
|
||||
spaceId: (connection.db.prepare("select id from chat_spaces where org_id = ? and name = 'general'").get(orgId) as { id: string }).id,
|
||||
threadId: threadId!,
|
||||
senderType: "employee",
|
||||
senderId: employeeId,
|
||||
bodyMd: "One more thing: schedule the rollout for Friday.",
|
||||
attachmentFileIds: [],
|
||||
});
|
||||
});
|
||||
const runsAfterSecondReply = connection.db.prepare("select count(*) as c from task_runs where org_id = ? and trigger = 'chat_mention'")
|
||||
.get(orgId) as { c: number };
|
||||
expect(runsAfterSecondReply.c).toBe(runsAfterReply.c);
|
||||
const wakeTasks = connection.db.prepare(
|
||||
"select count(distinct r.task_id) as c from task_runs r where r.org_id = ? and r.trigger = 'chat_mention'",
|
||||
).get(orgId) as { c: number };
|
||||
expect(wakeTasks.c).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user