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:
DottaandPaperclip authored and GitHub committed 2026-09-29 10:25:52 -05:00
1 parent 3b4b270650
commit 83076d7e7c
59 files changed
+2696 -83

No files matched your search

@@ -18,6 +18,7 @@ import { ISSUE_DISPOSITION_REPAIR_RETRY_REASON } from "@paperclipai/shared";
import { parseObject } from "../../../adapters/utils.js";
import { evaluateAgentInvokabilityFromDb } from "../../../services/agent-invokability.js";
import { budgetService } from "../../../services/budgets.js";
import { isCompletedOnboardingHandoffWake } from "../../../services/chat-completion-delivery.js";
import { isHeartbeatWakeOnDemandEnabled } from "../../../services/heartbeat-policy.js";
import { collectDispositionRepairSourceState } from "../../../services/recovery/disposition-repair.js";
import { legacyDispositionEpisode, legacyDispositionFingerprint } from "../../../services/recovery/legacy-continuation.js";
@@ -608,6 +609,10 @@ export function createPostgresRunDispatchAdapter(
isNonAssigneeWorkspaceBusyRetry: isNonAssigneeWorkspaceBusyRetry(retryReason, context),
resumeIntent,
wakeCommentIdPresent: Boolean(wakeCommentId),
isCompletedOnboardingHandoffWake: await isCompletedOnboardingHandoffWake(dbOrTx, {
companyId: input.companyId, issueId, agentId: input.agentId,
reason: wakeReason, contextSnapshot: context,
}),
continuationParkApplies,
continuationParksExecutor,
continuationSummaryBody,
@@ -436,6 +436,28 @@ describe("decideQueuedRunStaleness", () => {
expect(decideQueuedRunStaleness(facts, NOW)).toEqual({ stale: false });
});
describe("completed onboarding handoff report", () => {
const reportingFacts = (): QueuedRunFacts => ({
...baseStalenessFacts(), issueStatus: "done", wakeReason: "issue_children_completed",
isCompletedOnboardingHandoffWake: true,
});
it("allows a verified child-completion report without reopening the parent", () => {
expect(decideQueuedRunStaleness(reportingFacts(), NOW)).toEqual({ stale: false });
});
it.each([
{ overrides: { isCompletedOnboardingHandoffWake: false }, errorCode: "issue_terminal_status" },
{ overrides: { wakeReason: "issue_assigned" }, errorCode: "issue_terminal_status" },
{ overrides: { issueStatus: "cancelled" }, errorCode: "issue_terminal_status" },
{ overrides: { issueAssigneeAgentId: "agent-2" }, errorCode: "issue_assignee_changed" },
{ overrides: { retryReasonKind: "max_turn_continuation" as const }, errorCode: "issue_not_in_progress" },
])("preserves $errorCode guard with $overrides", ({ overrides, errorCode }) => {
expect(decideQueuedRunStaleness({ ...reportingFacts(), ...overrides }, NOW))
.toMatchObject({ stale: true, errorCode });
});
});
it("allows a non-assignee workspace-busy retry to bypass the ownership check", () => {
const facts: QueuedRunFacts = {
...baseStalenessFacts(),
@@ -151,6 +151,8 @@ export type QueuedRunFacts = {
resumeIntent: boolean;
wakeCommentIdPresent: boolean;
/** Verified from current company-scoped parent/child state immediately before dispatch. */
isCompletedOnboardingHandoffWake?: boolean;
/** True when the run's wake or retry reason asks for a continuation the parked-summary check must inspect. */
continuationParkApplies: boolean;
@@ -609,7 +611,10 @@ export function decideQueuedRunStaleness(
const statusOutcome = decideIssueStatus({
status: facts.issueStatus,
requiresInProgress,
terminalBypass: facts.resumeIntent || facts.wakeCommentIdPresent,
terminalBypass: facts.resumeIntent || facts.wakeCommentIdPresent || (
facts.isCompletedOnboardingHandoffWake === true &&
facts.wakeReason === "issue_children_completed" && facts.issueStatus === "done"
),
});
if (statusOutcome === "terminal") {
return {
@@ -1,4 +1,5 @@
import { isAcknowledgedNativeReassignmentStop, isAcknowledgedNativeStop } from "../../../services/acknowledged-native-stop.js";
import { isCompletedOnboardingHandoffWake } from "../../../services/chat-completion-delivery.js";
import { instanceSettingsService } from "../../../services/instance-settings.js";
import { currentConversationCommentCondition } from "../../../services/agent-conversations.js";
import { getExecutionBlocker } from "../../../services/execution-blocker.js";
@@ -413,6 +414,10 @@ function buildTransaction(tx: Db, deps: WakeQueuePostgresAdapterDeps, db: Db, ru
});
},
async isCompletedOnboardingHandoffWake(input) {
return isCompletedOnboardingHandoffWake(tx, input);
},
async reopenIssue({ companyId, issueId }) {
const updated = await issuesSvc.updateForCompany(issueId, companyId, { status: "todo", executionState: null }, tx);
return updated ? toIssueSnapshot(updated as unknown as IssueRow) : null;
@@ -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
) {