fix(recovery): persist board blocker user assignment

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-07-13 21:11:07 +00:00
1 parent 0c7d90397a
commit 675140afe4
2 files changed
+47 -1

No files matched your search

@@ -8,6 +8,7 @@ import {
agentRuntimeState,
agentWakeupRequests,
budgetPolicies,
companyMemberships,
companySecretBindings,
companySecrets,
companySkills,
@@ -422,6 +423,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
await db.delete(documents);
await db.delete(companySecretBindings);
await db.delete(companySecrets);
await db.delete(companyMemberships);
try {
await db.delete(companies);
break;
@@ -4430,6 +4432,13 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
defaultResponsibleUserId: "responsible-user",
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({
companyId,
principalType: "user",
principalId: "responsible-user",
status: "active",
membershipRole: "owner",
});
await db.insert(agents).values([
{
id: creatorAgentId,
@@ -4498,15 +4507,21 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
.where(eq(issues.id, blockerIssueId))
.then((rows) => rows[0] ?? null);
expect(blocker?.assigneeAgentId).toBeNull();
expect(blocker?.assigneeUserId).toBe("responsible-user");
// A board-attention comment must have been posted instead.
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, blockerIssueId));
expect(comments).toHaveLength(1);
expect(comments[0]?.body).toContain("Blocked on Board Review");
expect(comments[0]?.body).toContain("Board action required");
// No wakeup should have been enqueued for the creator agent.
const wakeups = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, creatorAgentId));
expect(wakeups).toHaveLength(0);
await heartbeat.reconcileStrandedAssignedIssues();
const allComments = await db.select().from(issueComments).where(eq(issueComments.issueId, blockerIssueId));
expect(allComments).toHaveLength(1);
});
it("assigns open unassigned blockers back to their creator agent when no responsibleUserId", async () => {
@@ -4520,9 +4535,16 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
id: companyId,
name: "Paperclip",
issuePrefix,
defaultResponsibleUserId: null,
defaultResponsibleUserId: "responsible-user",
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({
companyId,
principalType: "user",
principalId: "responsible-user",
status: "active",
membershipRole: "owner",
});
await db.insert(agents).values([
{
id: creatorAgentId,
+24
View File
@@ -985,6 +985,30 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
].join("\n"),
{},
);
const updated = await issuesSvc.update(candidate.id, {
assigneeUserId: candidate.responsibleUserId,
});
if (!updated) {
skipped += 1;
continue;
}
await logActivity(db, {
companyId: candidate.companyId,
actorType: "system",
actorId: "system",
agentId: null,
runId: null,
action: "issue.updated",
entityType: "issue",
entityId: candidate.id,
details: {
identifier: candidate.identifier,
assigneeUserId: candidate.responsibleUserId,
source: "recovery.reconcile_unassigned_blocking_issue",
},
});
skipped += 1;
continue;
}