mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
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:
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.",
|
||||
} : {}),
|
||||
};
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user