mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +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
@@ -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