fix(cursor): fence denied-turn recovery and attest deferred read input

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-03 16:03:32 -05:00
1 parent 2783390f88
commit c45cf9f418
11 files changed
+210 -12

No files matched your search

@@ -235,3 +235,28 @@ or remote cleanup is unconfirmed. This repairs the demonstrated loss of the
failed-create journal after the controller exited. It changes the harness only;
the provider packs, daemon, and Linux image remain frozen at `5623ff`. Remote
qualification and promotion are still pending.
### Demonstrated remote blockers after allocation recovered
The third remote denial attempt reached the native request and delivered the
exact rejection. The target stayed absent and the remote process observer proved
retirement. Its overall result remains failed: Cursor announced an empty read
card before streaming the bootstrap file path, and the strict single-file proof
could not attest that origin. The controller then applied generic missing-result
recovery after the denied turn ended, making two failed session-resume attempts.
The blocker repair completes only an entirely empty pending read origin from a
full single-path shape update before execution progress. Every attested notice
retains that same digest; late, changed, multi-path, and unsafe input stays
unproven. This changes passive evidence, not provider permissions or file access.
Committed Cursor permission declines followed by a completed turn without a
semantic result now fail the run once, keep the task open, and assign recovery to
the operator with no automatic wake. Other missing-result recovery remains intact.
The decision proof binds company, agent, run, turn, normalized session, source,
request, and strict event order. It does not assert response delivery or absence
of effects; those remain separate live requirements.
Focused verification passed: 534 controller tests, 43 Cursor runtime tests, and
43 fixture evidence tests, plus affected typechecks. The provider CLI pin,
native patch, and command/profile digest remain unchanged; the candidate source
and Node pack identities must be rebuilt before repeating affected qualification.
@@ -11,7 +11,7 @@ const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 &&
const nativeToolId = (v: unknown): string | undefined => typeof v === "string" && v.length > 0 && v.length <= 240 ? cursorToolIdentity(v) : undefined;
const digest = (s: string) => createHash("sha256").update(s).digest("hex");
type Fields = Record<string, string | boolean>;
type Tool = { kind?: string; commandSha256?: string; read?: SingleReadEvidence };
type Tool = { kind?: string; commandSha256?: string; read?: SingleReadEvidence; pendingReadNotice?: boolean };
type Permission = { requestId: string; kind?: string; hasInput: boolean; commandSha256?: string; declineOffered: boolean; requested?: boolean; outcome?: string; delivered?: boolean };
type ProjectionFailureReason =
| "evidence_limit" | "tool_limit" | "missing_command_origin" | "reused_tool_origin"
@@ -90,7 +90,15 @@ export function createCursorToolEvidence(binding: {
if (state.kind && state.kind !== call.kind) throw new ProjectionFailure("changed_tool_kind");
state.kind = call.kind;
}
if (state.kind === "read") state.read = updateSingleReadEvidence(state.read, call, binding.workingDirectory);
if (state.kind === "read") {
state.read = updateSingleReadEvidence(state.read, call, binding.workingDirectory);
if (state.read.pendingOriginInput && call.tag === "tool_call") state.pendingReadNotice = true;
if (state.pendingReadNotice && !state.read.pendingOriginInput) {
notice("tool", toolId, { status: "pending", operation: "read", ...(state.read.targetSha256 ? { readTargetSha256: state.read.targetSha256 } : {}) });
state.pendingReadNotice = false;
}
if (state.pendingReadNotice) return;
}
if (call.rawInput !== undefined && state.kind === "execute") {
const hash = command(call);
if (!hash || (call.tag !== "tool_call" && !state.commandSha256) || (state.commandSha256 && state.commandSha256 !== hash)) throw new ProjectionFailure("changed_or_missing_command");
@@ -30,6 +30,19 @@ it("does not establish proof from a late update or location/title without scalar
const state = updateSingleReadEvidence(undefined, { ...origin, rawInput: undefined, title: `Read ${path}` }, "/workspace");
expect(state).toEqual({}); expect(updateSingleReadEvidence(state, origin, "/workspace")).toEqual({});
});
it("completes an empty pending origin only from its full shape before execution progress", () => {
const pending = updateSingleReadEvidence(undefined, { ...origin, rawInput: {}, locations: undefined }, "/workspace");
expect(pending).toEqual({ pendingOriginInput: true });
expect(updateSingleReadEvidence(pending, { tag: "tool_call_update", rawInput: { path }, locations: [{ path }] }, "/workspace")).toEqual({ targetSha256: hash });
for (const status of ["in_progress", "completed", "failed"]) {
const unproven = updateSingleReadEvidence(pending, { tag: "tool_call_update", status }, "/workspace");
expect(updateSingleReadEvidence(unproven, { tag: "tool_call_update", rawInput: { path } }, "/workspace")).toEqual({});
}
for (const rawInput of [{ paths: [path, "private.txt"] }, { path, fileName: path }, { path: "../private.txt" }]) {
const unproven = updateSingleReadEvidence(pending, { tag: "tool_call_update", rawInput }, "/workspace");
expect(updateSingleReadEvidence(unproven, { tag: "tool_call_update", rawInput: { path } }, "/workspace")).toEqual({});
}
});
for (const [provider, create] of [["cursor", createCursorToolEvidence]] as const) {
describe(`${provider} passive single-read origin`, () => {
function setup() { const rows: any[] = []; const projector = create({ sessionId: "session", turnId: "turn", workingDirectory: "/workspace", active: () => true,
@@ -51,5 +64,14 @@ for (const [provider, create] of [["cursor", createCursorToolEvidence]] as const
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "other", kind: "read", status: "completed", rawInput: { path } });
expect(s.rows).toHaveLength(1); expect(s.rows[0].readTargetSha256).toBeUndefined();
});
it("publishes the pending origin after its delayed complete input, retaining exact proof on every notice", () => {
const s = setup();
s.projector.tool({ ...origin, rawInput: {}, locations: undefined });
expect(s.rows).toEqual([]);
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "read", rawInput: { path }, locations: [{ path }] });
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "read", status: "in_progress" });
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "read", status: "completed" });
expect(s.rows.map(row => [row.status, row.readTargetSha256])).toEqual([["pending", hash], ["in_progress", hash], ["completed", hash]]);
});
});
}
@@ -1,7 +1,7 @@
import { createHash } from "node:crypto";
import { safeAcpxLocations } from "./safe-locations.js";
export interface SingleReadEvidence { targetSha256?: string }
export interface SingleReadEvidence { targetSha256?: string; pendingOriginInput?: boolean }
const record = (value: unknown): Record<string, unknown> | undefined => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
const digest = (value: string) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
const scalarOptions = new Set(["offset", "limit", "startLine", "endLine"]);
@@ -26,13 +26,29 @@ function singleInputPath(value: unknown, cwd: string): string | undefined {
/** Passive attestation of the pinned single-file read input, never permission or
* filesystem authority. Multi-path tools and shell-output reads remain visible
* but cannot acquire this proof. Only the actual origin may establish a digest;
* but cannot acquire this proof. Only the actual origin's complete input establishes a digest;
* absent update fields retain it, while changed or ambiguous fields invalidate
* evidence without changing the provider's response or execution. */
* evidence without changing the provider's response or execution. An empty
* pending origin may acquire its complete input before execution progress. */
export function updateSingleReadEvidence(state: SingleReadEvidence | undefined, call: Record<string, unknown>, cwd: string): SingleReadEvidence {
if (!state) {
const path = call.tag === "tool_call" ? singleInputPath(call.rawInput, cwd) : undefined;
return path && locationsAgree(call.locations, cwd, path) ? { targetSha256: digest(path) } : {};
if (path && locationsAgree(call.locations, cwd, path)) return { targetSha256: digest(path) };
// Cursor announces an empty pending card before streaming its arguments.
// Only this wholly absent input can be completed by a shape update before
// execution progress; ambiguous or already executing reads stay unproven.
const input = record(call.rawInput);
const absent = call.rawInput === undefined || (input !== undefined && Object.keys(input).every(key => key === "path" && input[key] === undefined));
return call.tag === "tool_call" && call.status === "pending" && absent
&& (call.locations === undefined || (Array.isArray(call.locations) && call.locations.length === 0))
? { pendingOriginInput: true } : {};
}
if (state.pendingOriginInput) {
const path = call.tag === "tool_call_update" && (call.status === undefined || call.status === "pending")
? singleInputPath(call.rawInput, cwd) : undefined;
if (path && locationsAgree(call.locations, cwd, path)) return { targetSha256: digest(path) };
if (call.rawInput !== undefined || call.locations !== undefined || (call.status !== undefined && call.status !== "pending")) return {};
return state;
}
if (!state.targetSha256) return state; // A later delta cannot create an origin.
if (call.rawInput !== undefined) {
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import type { PrpEvent } from "../../vendor/paperclip-runner/index.js";
import { hasCompletedCursorPermissionDecline } from "./native-cursor-permission-decline.js";
const binding = { companyId: "company", runId: "run", agentId: "agent" };
function row(seq: number, eventType: string, payload: unknown): any {
const event = { schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceKind: "runner", runId: "run", turnId: "turn",
normalizedSessionId: "session", sourceInstanceId: "runner", sourceEventId: `runner:${seq}`, sourceSeq: seq, eventType, payload };
return { ...binding, seq, eventType, protocolSchemaVersion: 1, sourceInstanceId: "runner", sourceEventId: event.sourceEventId, sourceSeq: seq, payload: { prpEvent: event } };
}
function facts() {
const created = row(1, "runtime_request.created", { request: { requestId: "request", type: "permission", status: "pending", turnId: "turn",
origin: { adapter: "acpx-runtime-sidecar", provider: "cursor", method: "session/request_permission" } } });
const resolved = row(2, "runtime_request.resolved", { requestId: "request", action: "decline", turnId: "turn", requestKind: "permission_approval" });
const completed = row(3, "turn.completed", { status: "completed", error: null });
return { rows: [created, resolved, completed], terminal: structuredClone(completed.payload.prpEvent) as PrpEvent };
}
describe("completed Cursor permission decline", () => {
it("fences a committed denial without treating it as semantic success or requiring provider billing", () => {
const f = facts(); expect(hasCompletedCursorPermissionDecline(f.rows, binding, f.terminal)).toBe(true);
});
it.each(["company", "agent", "run", "turn", "session", "source", "provider", "method", "accept", "expired", "duplicate", "order", "terminal"])("rejects %s mismatches and ambiguous proof", mutation => {
const f = facts(), created = f.rows[0], resolved = f.rows[1];
if (mutation === "company") created.companyId = "foreign";
if (mutation === "agent") created.agentId = "foreign";
if (mutation === "run") created.runId = "foreign";
if (mutation === "turn") created.payload.prpEvent.turnId = "foreign";
if (mutation === "session") created.payload.prpEvent.normalizedSessionId = "foreign";
if (mutation === "source") resolved.sourceSeq = 99;
if (mutation === "provider") created.payload.prpEvent.payload.request.origin.provider = "claude";
if (mutation === "method") created.payload.prpEvent.payload.request.origin.method = "cursor/create_plan";
if (mutation === "accept") resolved.payload.prpEvent.payload.action = "accept";
if (mutation === "expired") { resolved.eventType = "runtime_request.expired"; resolved.payload.prpEvent.eventType = resolved.eventType; }
if (mutation === "duplicate") f.rows.push(structuredClone(resolved));
if (mutation === "order") resolved.seq = 4;
if (mutation === "terminal") f.terminal.sourceEventId = "foreign";
expect(hasCompletedCursorPermissionDecline(f.rows, binding, f.terminal)).toBe(false);
});
});
@@ -0,0 +1,55 @@
import { and, asc, eq, inArray } from "drizzle-orm";
import { heartbeatRunEvents, type Db } from "@paperclipai/db";
import type { PrpEvent } from "../../vendor/paperclip-runner/index.js";
const record = (value: unknown): Record<string, any> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
type Binding = { companyId: string; runId: string; agentId: string };
const TYPES = ["runtime_request.created", "runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired", "turn.completed"];
export class NativeCursorPermissionDeclinedError extends Error {
constructor() {
super("native_finalization_missing: session returned no semantic result; Cursor permission was declined; explicit direction is required");
this.name = "NativeCursorPermissionDeclinedError";
}
}
/** A committed user denial fences automatic disposition recovery. It does not
* claim the provider received the response or prove absence of file effects. */
export function hasCompletedCursorPermissionDecline(rows: readonly unknown[], binding: Binding, terminal: PrpEvent): boolean {
if (terminal.eventType !== "turn.completed" || !terminal.turnId || !terminal.normalizedSessionId || !terminal.sourceInstanceId) return false;
const selected = rows.map(record).map(row => ({ row, event: record(record(row.payload).prpEvent) })).filter(({ row, event }) =>
row.companyId === binding.companyId && row.agentId === binding.agentId && row.runId === binding.runId
&& row.protocolSchemaVersion === 1 && event.schema === "paperclip.prp.event.v1" && event.schemaVersion === 1
&& event.sourceKind === "runner" && event.runId === binding.runId && event.turnId === terminal.turnId
&& event.normalizedSessionId === terminal.normalizedSessionId && event.sourceInstanceId === terminal.sourceInstanceId
&& event.sourceEventId === row.sourceEventId && event.sourceInstanceId === row.sourceInstanceId && event.sourceSeq === row.sourceSeq
&& row.eventType === event.eventType && Number.isSafeInteger(row.seq) && Number.isSafeInteger(event.sourceSeq));
const terminals = selected.filter(({ event }) => event.eventType === "turn.completed" && event.sourceEventId === terminal.sourceEventId);
if (terminals.length !== 1) return false;
const end = terminals[0]!;
return selected.some(created => {
if (created.event.eventType !== "runtime_request.created") return false;
const request = record(record(created.event.payload).request), origin = record(request.origin);
if (request.type !== "permission" || request.status !== "pending" || !request.requestId || request.turnId !== terminal.turnId
|| origin.provider !== "cursor" || origin.method !== "session/request_permission"
|| !["acpx-runtime", "acpx-runtime-sidecar"].includes(origin.adapter)) return false;
if (selected.filter(({ event }) => event.eventType === "runtime_request.created" && record(record(event.payload).request).requestId === request.requestId).length !== 1) return false;
const outcomes = selected.filter(({ event }) => ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(event.eventType)
&& record(event.payload).requestId === request.requestId);
if (outcomes.length !== 1) return false;
const resolved = outcomes[0]!, payload = record(resolved.event.payload);
return resolved.event.eventType === "runtime_request.resolved" && payload.action === "decline"
&& payload.turnId === terminal.turnId && payload.requestKind === "permission_approval"
&& created.row.seq < resolved.row.seq && resolved.row.seq < end.row.seq
&& created.event.sourceSeq < resolved.event.sourceSeq && resolved.event.sourceSeq < end.event.sourceSeq;
});
}
export async function readCompletedCursorPermissionDecline(db: Db, binding: Binding, terminal: PrpEvent) {
const rows = await db.select().from(heartbeatRunEvents).where(and(
eq(heartbeatRunEvents.companyId, binding.companyId), eq(heartbeatRunEvents.runId, binding.runId), eq(heartbeatRunEvents.agentId, binding.agentId),
inArray(heartbeatRunEvents.eventType, TYPES),
)).orderBy(asc(heartbeatRunEvents.seq)).limit(1001);
if (rows.length > 1000) throw new Error("native_event_replay_conflict: permission recovery proof exceeds control-event budget");
return hasCompletedCursorPermissionDecline(rows, binding, terminal);
}
@@ -49,6 +49,7 @@ import {
NativeSessionProtocolIntegrityError,
} from "../../vendor/paperclip-runner/index.js";
import * as issueServiceModule from "../issues.js";
import { NativeCursorPermissionDeclinedError } from "./native-cursor-permission-decline.js";
import {
createNativeHarnessBackupStamp,
verifyNativeHarnessBackupStamp,
@@ -7374,6 +7375,20 @@ describe("native warm session supervision", () => {
});
describe("native session bounded recovery", () => {
it("leaves a denied Cursor task open and gives recovery to the operator without another provider attempt", async () => {
const updates: Array<{ table: unknown; values: Record<string, unknown> }> = [];
const failure = new NativeCursorPermissionDeclinedError();
state.execute.mockReset().mockRejectedValueOnce(failure);
state.upsertRecoveryAction.mockReset().mockResolvedValue({});
await expect(executePaperclipNativeSession({ db: leaseDb(execution, {}, {}, updates), execution, runnerInstanceId: "runner" })).rejects.toBe(failure);
expect(updates.find(update => update.table === nativeRunFinalizations && update.values.phase === "terminal_failure")?.values).toMatchObject({
failureCode: "native_permission_declined", nextAttemptAt: null,
failureDetail: { recoverable: false, nextAction: expect.stringContaining("Automatic recovery is stopped") },
});
expect(updates.some(update => update.table === issues && update.values.status === "blocked")).toBe(false);
expect(state.upsertRecoveryAction).toHaveBeenCalledWith(expect.objectContaining({ cause: "native_permission_declined", ownerType: "board", wakePolicy: null }));
expect(state.execute).toHaveBeenCalledTimes(1);
});
it.each(["operator", "reassignment"])("does not turn an acknowledged %s Stop before completion into a failure or a retry", async (source) => {
const updates: Array<{ table: unknown; values: Record<string, unknown> }> = [];
const stop: Record<string, unknown> = {};
@@ -1,5 +1,6 @@
import { nativeRetryCancellationEligible, rethrowNativeCancellationLockConflict, assertCancellationRequest, cancellationIntentId as callerCancellationIntentId, cancellationRequestId } from "./native-cancellation-request.js";
import { readNativeCursorPlanWait } from "./native-cursor-plan-wait.js";
import { NativeCursorPermissionDeclinedError, readCompletedCursorPermissionDecline } from "./native-cursor-permission-decline.js";
import { resolveAcpxQualification } from "./acpx-qualification.js";
import { readLocalAiCredentialFile } from "../local-ai-credential-file.js";
import { prepareGrokRunnerCredentials } from "./grok-runner-credentials.js";
@@ -6048,12 +6049,13 @@ export function nativeSessionFailureDisposition(
sourceFailureCode === "native_session_cleanup_quarantined" ||
sourceFailureCode === "native_adopted_runner_authentication_timeout" ||
sourceFailureCode === "native_provider_usage_limit";
const exhausted = permanentFailure || attempt >= 3;
const permissionDeclined = sourceFailureCode === "native_permission_declined";
const exhausted = permanentFailure || permissionDeclined || attempt >= 3;
return {
phase: exhausted
? ("terminal_failure" as const)
: ("retryable_failure" as const),
failureCode: permanentFailure
failureCode: permanentFailure || permissionDeclined
? sourceFailureCode!
: exhausted
? ("native_session_retry_exhausted" as const)
@@ -6072,6 +6074,7 @@ export function nativeSessionRecoveryProjection(input: {
exhausted,
issueStatus:
exhausted &&
input.failureCode !== "native_permission_declined" &&
input.failureCode !== NATIVE_ADOPTED_RUNNER_AUTHENTICATION_TIMEOUT
? ("blocked" as const)
: null,
@@ -6090,6 +6093,7 @@ export function nativeSessionRecoveryProjection(input: {
export function nativeSessionFailureSourceCode(
error: unknown,
):
| "native_permission_declined"
| "native_provider_terminal_failed"
| "native_provider_approval_required"
| "native_provider_usage_limit"
@@ -6113,6 +6117,7 @@ export function nativeSessionFailureSourceCode(
| "native_current_wake_comments_unread"
| "native_current_wake_comments_changed_after_read"
| "native_session_interrupted" {
if (error instanceof NativeCursorPermissionDeclinedError) return "native_permission_declined";
if (error instanceof NativeProviderTerminalFailure) {
if (error.providerCode === "approval_required") return "native_provider_approval_required";
// Failed terminals retain their security meaning across the provider facade.
@@ -8427,6 +8432,10 @@ async function executePaperclipNativeSessionWithinScope(
const planWait = await readNativeCursorPlanWait(input.db, input.execution.binding);
if (planWait?.source.terminalEventId === terminalEvent.sourceEventId) return planWait.result;
}
if (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor"
&& await readCompletedCursorPermissionDecline(input.db, input.execution.binding, terminalEvent)) {
throw new NativeCursorPermissionDeclinedError();
}
const [conversation] = await input.db
.select({ agentId: issues.conversationAgentId })
.from(issues)
@@ -8877,7 +8886,9 @@ async function executePaperclipNativeSessionWithinScope(
checkpointExists: recoveryEvidence.checkpointExists,
recoveryOwner: recoveryProjection.recoveryOwner,
nextAction:
sourceFailureCode === "native_provider_approval_required"
sourceFailureCode === "native_permission_declined"
? "Cursor permission was declined. Inspect the task and give explicit direction before starting further provider work. Automatic recovery is stopped."
: sourceFailureCode === "native_provider_approval_required"
? "Approval required. Review the operation and update the agent's permission setting before retrying. This runner has no interactive approval handler."
: sourceFailureCode === "native_session_cleanup_quarantined"
? NATIVE_CLEANUP_OPERATOR_RECOVERY_MESSAGE
@@ -9052,7 +9063,9 @@ async function executePaperclipNativeSessionWithinScope(
recoveryEvidence.providerSessionEstablished,
},
nextAction:
sourceFailureCode === "native_provider_approval_required"
sourceFailureCode === "native_permission_declined"
? "Cursor permission was declined. Inspect the task and give explicit direction before starting further provider work. Automatic recovery is stopped."
: sourceFailureCode === "native_provider_approval_required"
? "Approval required. Review the operation and update the agent's permission setting before retrying. This runner has no interactive approval handler."
: sourceFailureCode === "native_session_cleanup_quarantined"
? NATIVE_CLEANUP_OPERATOR_RECOVERY_MESSAGE
@@ -126,6 +126,9 @@ it("requires a correlated failed semantic finalization after the delivered denia
const input = { runId: "run", turnId: "turn", requestId: "request", issue: { id: "issue", status: "in_progress" },
run: { id: "run", nativeIssueId: "issue", runtimeMode: "native", status: "failed", errorCode: "native_session_interrupted", error: "native_finalization_missing: session returned no semantic result" }, events: [evidence, terminal] };
expect(hasCursorDeniedTurnTerminal(input)).toBe(true);
const declined = { ...input, run: { ...input.run, errorCode: "native_permission_declined", error: "native_finalization_missing: session returned no semantic result; Cursor permission was declined; explicit direction is required" } };
expect(hasCursorDeniedTurnTerminal(declined)).toBe(true);
expect(hasCursorDeniedTurnTerminal({ ...declined, events: [terminal] })).toBe(false);
for (const status of ["succeeded", "cancelled", "timed_out", "running"]) expect(hasCursorDeniedTurnTerminal({ ...input, run: { ...input.run, status } })).toBe(false);
expect(hasCursorDeniedTurnTerminal({ ...input, issue: { ...input.issue, status: "done" } })).toBe(false);
expect(hasCursorDeniedTurnTerminal({ ...input, run: { ...input.run, errorCode: "other" } })).toBe(false);
+3 -1
View File
@@ -73,9 +73,11 @@ export function hasCursorDeniedTurnTerminal(input: {
run: unknown; issue: unknown; events: readonly unknown[]; runId: string; turnId: string; requestId: string;
}): boolean {
const run = rec(input.run), issue = rec(input.issue);
const missingResult = run.errorCode === "native_session_interrupted" && run.error === "native_finalization_missing: session returned no semantic result";
const declined = run.errorCode === "native_permission_declined" && run.error === "native_finalization_missing: session returned no semantic result; Cursor permission was declined; explicit direction is required";
if (!id(input.runId) || !id(input.turnId) || !id(input.requestId) || run.id !== input.runId || run.nativeIssueId !== issue.id
|| run.status !== "failed" || run.runtimeMode !== "native" || issue.status !== "in_progress"
|| run.errorCode !== "native_session_interrupted" || run.error !== "native_finalization_missing: session returned no semantic result") return false;
|| (!missingResult && !declined)) return false;
const terminals = input.events.map(rec).map(row => ({ row, event: rec(rec(row.payload).prpEvent) }))
.filter(({ event }) => ["turn.completed", "turn.failed", "turn.cancelled", "turn.interrupted"].includes(event.eventType));
const delivered = readCursorToolEvidence(input.events, input.runId).filter(notice => notice.stage === "permission_delivered"
+1 -1
View File
@@ -838,7 +838,7 @@ async function runAttempt(input: {
Object.assign(publishedResult, {
status: "failed",
failureClass: "cleanup_failure",
error: message,
error: publishedResult.error ? `${publishedResult.error}; ${message}` : message,
cleanup: "failed",
} satisfies Partial<RunnerE2EResult>);
const safeResult = `${JSON.stringify(