mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 00:26:28 +01:00
fix(dispatch): let the answer to the agent's own question continue the task it blocked on (#20931)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - An agent can ask the person a question (`request_human_input`) and wait for the answer > - A later comment from the person moves an open question into history (#15392). The agent still needs the answer, so it reports a blocker and the task becomes Blocked > - The person then answers. Dispatch cancels the answer's continuation with `issue_not_in_progress`, because only a dependency-blocked task may take an answer while Blocked > - The task rests Blocked with a failed run and a Retry. The answer is lost until a person clicks Retry > - This pull request lets the answer continue the task when the same agent blocked it to wait for that answer, proven from durable records > - The benefit is that the person's answer always reaches the agent that asked, without a Retry ## Linked Issues or Issue Description Refs #15392, Refs #20872 **What happened?** Restart-chaos waves with no fault injected fail every W2Q task on every provider (Claude, OpenCode, Codex) with `R3:retry_needed`. For Claude task t03 on master7e8901aa60: run 5aeb9244 asks a question and the task goes `in_review`. A board comment ("I will answer shortly; do not write the brief yet") wakes run d728886f. That comment makes the question historical, so `paperclip_finish` with a `response_wake` wait is rejected ("without a pending wait condition"). The agent then calls `paperclip_block`; the status decision `current_track_blocker_waiting` sets the task Blocked. The board answers. Continuation run adbda5de is cancelled: "resolved-interaction continuation issue is no longer in_progress (current status: blocked)". The task rests blocked/failed with Retry. **What did you expect to happen?** The answer continues the task. The agent writes the brief with the answer and finishes. **Steps to reproduce** 1. An agent asks a question on its task. 2. The person posts a comment before answering. 3. The agent reports a blocker while it waits for the answer. 4. The person answers the question. 5. The answer's continuation run is cancelled with `issue_not_in_progress`. ## What Changed - Status committer (`status-decision-committer.ts`): when it applies an agent-reported, board-owned blocker (`current_track_blocker_waiting` or `task_wide_blocker_bound`), it records the agent's own pending, undismissed ordinary questions on the task as `awaitingInteractionIds` in the decision JSON, in the same transaction under the issue lock (`questionsAwaitedByAgentBlock`). - New `agentBlockingQuestionState` (`server/src/services/blocked-question-answer.ts`). It reads only server-owned rows. It returns the verified `lastStatusDecisionId` and `statusVersion` when: the answered interaction was created by this agent on this task and resolved by a person; the task is Blocked and still assigned to the agent; the task's `lastStatusDecisionId` is an applied Blocked decision with one of the two reason codes, made by a run of the same agent, whose `awaitingInteractionIds` contains this interaction; and the task's `statusVersion` still equals that decision's `projectedStatusVersion` (nothing changed the status since). - `decideQueuedRunStaleness` (`server/src/modules/run-dispatch/domain/policy.ts`): the Blocked exemption for an answer continuation also accepts this fact (`isAgentBlockingQuestionAnswer`). The postgres adapter loads it under the issue lock. - `heartbeat.ts` resolved-interaction checkout: for that answer, `blocked` is an accepted expected status, and the verified state is passed to `issues.checkout` as `blockedState`. - `issues.checkout`: with `blockedState`, the guarded update accepts a Blocked row only when `lastStatusDecisionId` and `statusVersion` still match, so a person who re-blocks the task in between keeps the new block. When the expected statuses include `blocked`, a checkout from Blocked also clears `unblockDescriptor`, `blockedTransitionAt` and `blockedOwnerNotifiedAt`, as an ordinary status change from Blocked does. - `doc/execution-semantics.md` describes the rule. ## Verification - New integration test `server/src/__tests__/heartbeat-blocked-question-answer.test.ts` (embedded Postgres, real `commitNativeStatusDecision`): the agent's blocker decision makes the task Blocked, the person answers, the answer is delivered through `heartbeat.wakeup` with the same context shape as staging. - Red on master (product hunks removed, test kept): `delivers the answer when the agent blocked the task to wait for it` fails with `status: cancelled, errorCode: issue_not_in_progress`, the same code as the chaos run. - Green: 5/5 pass. The answer run succeeds, the adapter runs, and the blocker fields are cleared. Negative cases still cancel with `issue_not_in_progress`: a person re-blocked the task after the agent, the question did not exist when the agent blocked, and the task is done. A checkout with a stale verified state conflicts and keeps the person's new blocker. - `policy.test.ts`: new case for the fact (continue when set, cancel when unset, reassignment, backlog/done/cancelled). - `cd server && pnpm exec vitest run src/modules/run-dispatch src/__tests__/heartbeat-dependency-scheduling.test.ts`: 208 passed. - `cd server && pnpm exec vitest run src/__tests__/issues-service.test.ts src/__tests__/heartbeat-stale-queue-invalidation.test.ts src/services/explicit-native-continuation.test.ts`: 656 passed. - `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes. - Chaos, local, seed 5101, `--workloads W2Q --faults K0 --adapters codex,claude --lifecycle warm`, on722303e4a2(first head): wave w-20261111T204405-s5101, 6/6 PASS (3 Codex, 3 Claude). Every task went in_review -> blocked -> in_progress -> done without Retry. Before: the same local seed on master (w-20261111T190352-s5101) failed 3/3 Codex W2Q cells with `R3:retry_needed`, and Daytona Claude wave w-20261111T200553-s6101 failed 2/3 W2Q cells the same way. ## Risks - Low to medium. Only a resolved interaction continuation on a Blocked task changes behavior, and only when the durable linkage holds. If a board-owned blocker about something else and the agent's own question are open together, the answer resumes the agent; it then reports its blocker again. The blocker report carries no interaction reference, so the server cannot tell them apart; cancelling the answer instead loses it. A person's Blocked status, a block from before the question, dependency blocks, reassignment, done and cancelled keep the old cancel. - State saved by an older release: no schema change and no migration. A Blocked decision saved before this change has no `awaitingInteractionIds`, so its answer keeps the old behavior (cancel, Retry works). - The checkout change only applies when a caller passes `blocked` as an expected status. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution in an agent harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b01959cb15
commit
228b0169be
9 files changed
+327
-2
No files matched your search
@@ -26,6 +26,7 @@ import { normalizePrpResultSignals, type PrpStructuredRunResult } from "../../ve
|
||||
import { readNativeMonitorDelivery, readNativePreservedMonitorWait, readNativeSupersededMonitorWait, type NativeSupersededMonitorWait, type NativeMonitorDelivery, type NativePreservedMonitorWait } from "./native-monitor-delivery.js";
|
||||
import { readNativePlanWait, type NativePlanWaitSource } from "./native-plan-wait.js";
|
||||
import { activeIssueInteractionCondition } from "../issue-question-context.js";
|
||||
import { questionsAwaitedByAgentBlock } from "../blocked-question-answer.js";
|
||||
import { isConversation } from "../agent-conversations.js";
|
||||
import { hasPendingNativeChildCompletion, type NativeChildCompletionRecipient } from "./native-child-completion-delivery.js";
|
||||
import { and, asc, desc, eq, inArray, notInArray, or, sql } from "drizzle-orm";
|
||||
@@ -2159,12 +2160,17 @@ export async function commitNativeStatusDecision(input: {
|
||||
})
|
||||
.where(eq(nativeRunFinalizations.runId, input.runId));
|
||||
};
|
||||
const awaitingInteractionIds = await questionsAwaitedByAgentBlock(tx as unknown as Db, {
|
||||
companyId: input.companyId, issueId: input.issueId, agentId: originRun?.agentId, reasonCode,
|
||||
toStatus: input.decision.toStatus, unblockOwner: input.decision.unblockDescriptor?.owner,
|
||||
});
|
||||
const decisionJson = {
|
||||
statusAction: input.decision.statusAction,
|
||||
toStatus: input.decision.toStatus,
|
||||
reasonCode,
|
||||
unblockDescriptor: input.decision.unblockDescriptor,
|
||||
effects: input.decision.effects,
|
||||
...(awaitingInteractionIds.length ? { awaitingInteractionIds } : {}),
|
||||
...(externalChatReviewPresentation
|
||||
? { externalChatReviewPresentation }
|
||||
: {}),
|
||||
|
||||
Reference in new issue
Block a user