mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 20:50:08 +02:00
feat: return completed handoffs to Agent Chat (#14408)
Return completed Agent Chat handoffs through a durable outbox and scope each generated update to its supplied tasks. Add recovery, browser delivery, result access, and calibrated quality coverage. Validated with two consecutive ten-case Claude/Codex campaigns, all CI checks, and a 5/5 review. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3b4b270650
commit
83076d7e7c
59 files changed
+2696
-83
No files matched your search
@@ -167,6 +167,9 @@ export interface WakeQueueTransaction {
|
||||
commentIds: string[];
|
||||
}): Promise<boolean>;
|
||||
reopenIssue(input: { companyId: string; issueId: string; runId: string }): Promise<IssueSnapshot | null>;
|
||||
/** Verifies a Done onboarding parent's completion wake against its own completed children. */
|
||||
isCompletedOnboardingHandoffWake(input: { companyId: string; issueId: string; agentId: string;
|
||||
reason: string | null; contextSnapshot: Record<string, unknown> }): Promise<boolean>;
|
||||
/**
|
||||
* Atomically claims the wake for promotion, guarded on its current
|
||||
* `deferred_issue_execution` status. Call this before any other write in
|
||||
|
||||
@@ -99,6 +99,7 @@ function createFakeHost(overrides: Partial<WakeQueueHost> = {}): WakeQueueHost {
|
||||
function createFakeTransaction(overrides: Partial<WakeQueueTransaction> = {}): WakeQueueTransaction {
|
||||
return {
|
||||
findInvokableAgent: vi.fn(async () => AGENT),
|
||||
isCompletedOnboardingHandoffWake: vi.fn(async () => false),
|
||||
findNextDeferredWake: vi.fn(async () => null),
|
||||
getQueuedCommentLiveness: vi.fn(async () => ({ liveNonSelfCommentIds: [], containedSelfAuthoredComment: false })),
|
||||
cancelDeferredWake: vi.fn(async () => true),
|
||||
@@ -510,6 +511,27 @@ describe("releaseIssueExecution", () => {
|
||||
expect(result.outcome.kind).toBe("released");
|
||||
});
|
||||
|
||||
it.each(["verified", "unverified", "cancelled", "ordinary-task"])("handles a completed onboarding handoff report: %s", async scenario => {
|
||||
const queue = [wakeCandidate({ agentId: ISSUE.assigneeAgentId!, requestedByActorType: "system",
|
||||
reason: "issue_children_completed", wakeReason: "issue_children_completed",
|
||||
deferredContextSeed: { completedChildIssueId: "child", onboardingCompletion: true } })];
|
||||
const transaction = createFakeTransaction({
|
||||
findNextDeferredWake: vi.fn(async () => queue.shift() ?? null),
|
||||
isCompletedOnboardingHandoffWake: vi.fn(async () => scenario !== "unverified"),
|
||||
});
|
||||
const release = createReleaseIssueExecution({
|
||||
issueLock: createFakeIssueLock(createFakeHost(), transaction, { ...ISSUE,
|
||||
originKind: scenario === "ordinary-task" ? "manual" : "onboarding_first_task",
|
||||
status: scenario === "cancelled" ? "cancelled" : "done" }),
|
||||
recovery: createFakeRecovery(),
|
||||
});
|
||||
const result = await release({ companyId: RUN.companyId, runId: RUN.id, now: new Date() });
|
||||
expect(result.outcome.kind).toBe(scenario === "verified" ? "promoted" : "released");
|
||||
expect(transaction.reopenIssue).not.toHaveBeenCalled();
|
||||
expect(transaction.finalizePromotedWake).toHaveBeenCalledTimes(scenario === "verified" ? 1 : 0);
|
||||
expect(transaction.cancelDeferredWake).toHaveBeenCalledTimes(scenario === "verified" ? 0 : 1);
|
||||
});
|
||||
|
||||
it("reopens a completed task before promoting its assignee's human follow-up", async () => {
|
||||
const queue = [wakeCandidate({
|
||||
agentId: ISSUE.assigneeAgentId!,
|
||||
|
||||
@@ -356,8 +356,13 @@ async function promoteDeferredWake(
|
||||
// after completion; it cannot revive a cancelled task. Other stale
|
||||
// continuations cannot revive assignee execution. Cancel before claiming promotion so
|
||||
// the compare-and-set still sees the deferred wake.
|
||||
const onboardingResultReport = currentIssue.status === "done" && currentIssue.originKind === "onboarding_first_task" &&
|
||||
await ports.transaction.isCompletedOnboardingHandoffWake({ companyId: run.companyId, issueId: currentIssue.id,
|
||||
agentId: workingCandidate.agentId, reason: workingCandidate.wakeReason ?? workingCandidate.reason,
|
||||
contextSnapshot: workingCandidate.deferredContextSeed });
|
||||
if (
|
||||
!shouldReopen &&
|
||||
!onboardingResultReport &&
|
||||
(currentIssue.status === "done" || currentIssue.status === "cancelled") &&
|
||||
workingCandidate.agentId === currentIssue.assigneeAgentId
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user