fix: distinguish cancelled dependencies from pending work

Identify cancelled blocker IDs in dependency receipts and require removing or replacing them instead of waiting for a completion wake. Preserve unresolved status, other blockers, idempotent snapshots and prior receipt compatibility.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-05 12:19:53 -05:00
1 parent 245e8568d9
commit aba7ec219b
9 files changed
+49 -14

No files matched your search

@@ -27,6 +27,10 @@ is already ready, the receipt directs the parent to review and continue instead
of blocking for an event that already happened. Additional teammate work needs
an assigned revision task; a comment on a completed task does not substitute for
that assignment. Idempotent replay preserves the original readiness snapshot.
Cancelled dependencies remain unresolved and appear separately in
`cancelledTaskIds`. Their guidance requires removing an obsolete edge or assigning
a replacement while preserving other required blockers. Cancellation does not
count as success and cannot be awaited as a future completion wake.
This is a new combined instruction and receipt correction, not a regrade of the
failed experiment. The original results above remain unchanged. A new matched
@@ -1 +1 @@
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
@@ -1823,6 +1823,16 @@
"additionalProperties": false,
"description": "Readiness when this update was applied. A replay retains that snapshot; task state can change afterward.",
"properties": {
"cancelledTaskIds": {
"description": "Unresolved dependencies that are cancelled and cannot produce a completion wake.",
"items": {
"minLength": 1,
"type": "string"
},
"maxItems": 200,
"type": "array",
"uniqueItems": true
},
"isReady": {
"type": "boolean"
},
@@ -24,7 +24,7 @@
"prpVersion": 1,
"nativeExecutionVersion": 1,
"catalogVersion": 1,
"catalogSha256": "sha256:4511b5443cd8679235adace402e8bb926d2967de00a71d324b97deb1900e39ff",
"catalogSha256": "sha256:0bf1f51eae45c4c84bf748d1f9454e1a8e41a73ae440230b184cbdfe914a9a3d",
"driverContractVersion": 1,
"driverKind": "paperclip-deterministic",
"driverVersion": "1.0.0"
@@ -160,7 +160,7 @@
},
{
"path": "fixtures/evals/native-execution-seeded.json",
"sha256": "031074d68aa769ce5fba01281a087b6b12a4863f428b41dfee9de5b5822e0a63",
"sha256": "305f6cd36b657fce5ca897be62bec636481e7f3e65715754791b1b2e216a7635",
"expectation": "accept",
"compatibilityCase": "canonical"
},
@@ -146,6 +146,13 @@ export const setDependenciesAction = {
"items": { "type": "string", "minLength": 1 },
"maxItems": 200,
"uniqueItems": true
},
"cancelledTaskIds": {
"type": "array",
"description": "Unresolved dependencies that are cancelled and cannot produce a completion wake.",
"items": { "type": "string", "minLength": 1 },
"maxItems": 200,
"uniqueItems": true
}
},
"required": ["isReady", "unresolvedTaskIds"],
@@ -23,7 +23,7 @@ export function nativeToolContractFingerprintForTarget(
return `sha256:${createHash("sha256")
.update(
JSON.stringify({
schema: "paperclip.native-tool-contract.v16",
schema: "paperclip.native-tool-contract.v17",
executionTargetKind,
advertisementPolicy: {
// Direct provider threads retain declarations from thread/start.
@@ -37,7 +37,7 @@ export function nativeToolContractFingerprintForTarget(
structuredHumanInput:
"always_advertised_run_issue_agent_binding_gated_current_task_description.v2",
semanticCompletion: "finish_accessible_deliverable_evidence.v4",
nativeProcedures: "hiring_delegation_and_dependency_readiness_guidance.v2",
nativeProcedures: "hiring_delegation_and_dependency_readiness_guidance.v3",
connectorTools: "assigned_resources_and_pinned_skill_bundle.v1",
...(executionTargetKind === "remote"
? { assignedMcpTools: "codex_server_gateway_prp_relay.v1" }
@@ -1044,7 +1044,7 @@ describe("PaperclipRunnerToolAuthority", () => {
},
});
expect(dependencyReceipt).toMatchObject({
dependencyReadiness: { isReady: false, unresolvedTaskIds: [prerequisiteId] },
dependencyReadiness: { isReady: false, unresolvedTaskIds: [prerequisiteId], cancelledTaskIds: [] },
guidance: expect.stringContaining("Recorded dependencies are unfinished"),
});
expect(dependencyReceipt).toMatchObject({ guidance: expect.stringContaining("do not sleep or poll") });
@@ -1070,7 +1070,7 @@ describe("PaperclipRunnerToolAuthority", () => {
arguments: { idempotencyKey: "observe-finished-prerequisite", blockedByTaskIds: [prerequisiteId] },
});
expect(readyReceipt).toMatchObject({
dependencyReadiness: { isReady: true, unresolvedTaskIds: [] },
dependencyReadiness: { isReady: true, unresolvedTaskIds: [], cancelledTaskIds: [] },
scheduledWakeIds: [],
guidance: expect.stringContaining("do not block waiting for these completed tasks"),
});
@@ -1080,13 +1080,17 @@ describe("PaperclipRunnerToolAuthority", () => {
})).resolves.toEqual(dependencyReceipt);
const dependentId = (dependent as { task: { id: string } }).task.id;
await issueService(db).update(dependentId, { status: "cancelled", actorAgentId: agentId });
await expect(authority.execute({
const cancelledReceipt = await authority.execute({
tool: "set_dependencies", callId: "mixed-complete-and-cancelled",
arguments: { idempotencyKey: "mixed-complete-and-cancelled", blockedByTaskIds: [prerequisiteId, dependentId] },
})).resolves.toMatchObject({
dependencyReadiness: { isReady: false, unresolvedTaskIds: [dependentId] },
guidance: expect.stringContaining("Recorded dependencies are unfinished"),
});
expect(cancelledReceipt).toMatchObject({
dependencyReadiness: { isReady: false, unresolvedTaskIds: [dependentId], cancelledTaskIds: [dependentId] },
guidance: expect.stringContaining("Do not wait for those tasks"),
});
expect(cancelledReceipt).toMatchObject({ guidance: expect.stringContaining("preserving every other required blocker") });
expect(cancelledReceipt).toMatchObject({ guidance: expect.stringContaining("Do not treat cancellation as successful completion") });
expect((cancelledReceipt as { guidance: string }).guidance).not.toContain("call paperclip_block");
await expect(
authority.execute({
tool: "create_task",
@@ -1109,7 +1113,7 @@ describe("PaperclipRunnerToolAuthority", () => {
arguments: { idempotencyKey: "clear-finished-prerequisite", blockedByTaskIds: [] },
});
expect(cleared).not.toHaveProperty("guidance");
expect(cleared).toMatchObject({ dependencyReadiness: { isReady: true, unresolvedTaskIds: [] } });
expect(cleared).toMatchObject({ dependencyReadiness: { isReady: true, unresolvedTaskIds: [], cancelledTaskIds: [] } });
await expect(issueService(db).getRelationSummaries(issueId)).resolves.toMatchObject({ blockedBy: [] });
const nextRunId = "00000000-0000-4000-8000-000000000106";
@@ -38,7 +38,7 @@ import { badRequest, forbidden, notFound, HttpError } from "../../errors.js";
import { searchRunnerApi } from "./runner-api-catalog.js";
import { executeRunnerApi, validateRunnerApiCall, RUNNER_API_MAX_BYTES, type RunnerApiFile } from "./runner-api-client.js";
import { acquireRunnerApiResponseSlot, runnerApiCompanyCaptureMaxBytes, RUNNER_API_RESPONSE_MAX_BYTES, RUNNER_API_RESPONSE_RUN_MAX_BYTES, RunnerApiResponseLimitError } from "./runner-api-response-limits.js";
import { and, desc, eq, isNull, notInArray, sql } from "drizzle-orm";
import { and, desc, eq, inArray, isNull, notInArray, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
activityLog,
@@ -1334,6 +1334,13 @@ export class PaperclipRunnerToolAuthority {
}, tx);
if (!updated) throw new Error("paperclip_runner_task_not_found");
const readiness = await issueService(tx).getDependencyReadiness(updated.id, tx);
const cancelledBlockers = readiness.unresolvedBlockerIssueIds.length > 0
? await tx.select({ id: issues.id }).from(issues).where(and(
eq(issues.companyId, this.binding.companyId),
inArray(issues.id, readiness.unresolvedBlockerIssueIds),
eq(issues.status, "cancelled"),
)).orderBy(issues.id)
: [];
return {
commandId: `set-dependencies:${updated.id}:${updated.statusVersion}`,
disposition: "applied",
@@ -1343,11 +1350,14 @@ export class PaperclipRunnerToolAuthority {
dependencyReadiness: {
isReady: readiness.isDependencyReady,
unresolvedTaskIds: readiness.unresolvedBlockerIssueIds,
cancelledTaskIds: cancelledBlockers.map((blocker) => blocker.id),
},
...(blockedByIssueIds.length > 0 ? {
guidance: readiness.isDependencyReady
? "All recorded dependencies are complete and their workspaces are finalized at this update. Review the latest results and continue; do not block waiting for these completed tasks. If more teammate work is required, create a revision task with create_task and record its dependency. A comment on a completed task is not a replacement for assigning new work."
: "Recorded dependencies are unfinished at this update. Complete independent work, then call paperclip_block with the child agent as owner and child completion as the unblock action. End the turn to release the workspace; do not sleep or poll. Check current task state before blocking if a result has arrived. Paperclip resumes the parent when dependencies complete; review the latest results and pending feedback before finishing.",
: cancelledBlockers.length > 0
? "The cancelledTaskIds dependencies are cancelled and will not produce a completion wake. Do not wait for those tasks. Review whether their work is still required, then use set_dependencies to remove obsolete dependencies or replace them with newly assigned tasks while preserving every other required blocker. Do not treat cancellation as successful completion. If a human decision is required, request it explicitly."
: "Recorded dependencies are unfinished at this update. Complete independent work, then call paperclip_block with the child agent as owner and child completion as the unblock action. End the turn to release the workspace; do not sleep or poll. Check current task state before blocking if a result has arrived. Paperclip resumes the parent when dependencies complete; review the latest results and pending feedback before finishing.",
} : {}),
};
});