mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 07:12:00 +01:00
fix(recovery): accept a person's own answer at dispatch, not only at admission
Admission accepted a person's answer to a held task's pending decision, but buildExecutionContinuation only accepted interaction receipts that came through a saved interrupt queue, so the successor failed with continuation_user_authorization_missing before the agent saw the answer. Dispatch now accepts the same receipt when the interaction was resolved by that person (not an agent), its status matches the wake, and the authorization names this successor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac17aebe69
commit
60beb77303
2 files changed
+16
-3
No files matched your search
@@ -501,9 +501,15 @@ export async function buildExecutionContinuation(input: {
|
||||
: value.interactionId
|
||||
? interactions.some(interaction => interaction.id === value.interactionId && interaction.id === triggerInteraction?.id &&
|
||||
["accepted", "answered", "rejected"].includes(interaction.status) &&
|
||||
interruptQueues.some(queue => queue.id === value.queuedCommentInterruptId && queue.runId === value.runId &&
|
||||
object(queue.payload?.queuedCommentInterrupt).actorId === value.actorId &&
|
||||
queuedInteractionId(queue.payload) === interaction.id && queue.payload?.interactionStatus === interaction.status))
|
||||
(value.queuedCommentInterruptId
|
||||
? interruptQueues.some(queue => queue.id === value.queuedCommentInterruptId && queue.runId === value.runId &&
|
||||
object(queue.payload?.queuedCommentInterrupt).actorId === value.actorId &&
|
||||
queuedInteractionId(queue.payload) === interaction.id && queue.payload?.interactionStatus === interaction.status)
|
||||
// A person's own answer to the held task's pending decision:
|
||||
// bound to that person, this successor and the answered status.
|
||||
: interaction.resolvedByUserId === value.actorId && !interaction.resolvedByAgentId &&
|
||||
string(input.context.interactionStatus) === interaction.status &&
|
||||
priorRuns.some(run => run.id === value.runId)))
|
||||
: rows.some(comment => comment.id === value.commentId &&
|
||||
comment.authorType === "user" &&
|
||||
(!("commentUpdatedAt" in value || "commentBodyHash" in value || value.automaticRetry) || (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { taskInterruptionPresentation } from "./task-interruption-presentation.j
|
||||
import { readSystemRecovery } from "./system-recovery-state.js";
|
||||
import { reconcileRetainedWorkspaces } from "./workspace-recovery.js";
|
||||
import { requestTaskRetry } from "./task-retry.js";
|
||||
import { buildExecutionContinuation } from "./execution-continuation.js";
|
||||
import { useWorkspaceRecoveryFixture, action, db, run, seed } from "./__tests__/workspace-recovery-fixture.js";
|
||||
|
||||
const targets = vi.hoisted(() => new Map<string, AdapterSandboxExecutionTarget>());
|
||||
@@ -130,6 +131,12 @@ describe("a held task with a pending decision after its remote runner was lost",
|
||||
evidence: { explicitUserContinuation: { actorId: "local-board", interactionId: f.decision.id, previousRunId: f.runId } } });
|
||||
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.companyId),
|
||||
eq(activityLog.action, "issue.execution_recovery_settled"), eq(activityLog.actorId, "local-board")))).toHaveLength(1);
|
||||
// Dispatch rebuilds the continuation under the successor's identity and
|
||||
// rechecks the user authorization; the agent then receives the answer.
|
||||
const envelope = await buildExecutionContinuation({ db, companyId: f.companyId, issueId: f.issueId, agentId: f.agentId,
|
||||
runId: successor.id, context: successor.contextSnapshot ?? {}, summary: null, exposeLowTrustRaw: false });
|
||||
expect(envelope).toMatchObject({ trigger: { interactionId: f.decision.id }, interruptedRunId: f.runId });
|
||||
expect(envelope.interactionOutcomes).toEqual(expect.arrayContaining([expect.objectContaining({ id: f.decision.id })]));
|
||||
}, 60_000);
|
||||
|
||||
it("keeps the hold when the wake's user did not give the answer", async () => {
|
||||
|
||||
Reference in new issue
Block a user