mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 20:34:57 +02:00
fix(cursor): select failure diagnostics after trace notices (#14636)
## Thinking Path > - Paperclip manages work performed by AI agents. > - The Cursor CLI adapter turns process output into run results. > - Cursor can print a trace-file notice before a real error. > - The adapter used the first stderr line as the failure summary. > - This could hide the error behind an informational file path. > - This change selects the first diagnostic after that known notice and preserves the full logs. ## Linked Issues or Issue Description **What happened?** When Cursor exits with a nonzero code, a leading `cursor-retrieval: tracing to ...` notice can become the error summary. A later error remains in stderr but is absent from the summary. If the notice is the only output, the summary does not explain that the process exited unsuccessfully. **Expected behavior** Prefer the structured error, then a stderr diagnostic, then the exit code. Keep the run failed and preserve the original logs. **Steps to reproduce** 1. Use a fixture Cursor executable that writes the trace-file notice to stderr. 2. Write `Authentication failed` on the next line, then exit with code 7. 3. The old adapter reports the trace-file notice. This change reports the authentication error. 4. Repeat with only the notice. This change reports `Cursor exited with code 7`. **Paperclip version or commit** Reproduced against master commit `17780751551b3bc1c2521f7694026c34534c46c9` with local process fixtures. **Deployment mode** Local CLI adapter. The diagnostic helper is also used by environment probes. Searched open Cursor PRs and issues. PRs #14631 and #14435 concern native ACP support; #11106 concerns MCP configuration. None changes this legacy CLI diagnostic selection. ## What Changed - Skip only the exact trace-location notice when choosing a diagnostic line. - Remove terminal control codes from summary candidates. - Use the same selection for execution and environment probes. - Preserve structured-error priority, exit status, retry decisions, and raw stdout/stderr. - Add child-process regression tests and narrow parsing cases. Document the behavior. ## Verification - Two execution regression cases failed on the previous implementation; structured-error priority already passed. - `pnpm exec vitest run packages/adapters/cursor-local`: all 16 tests passed across five files. - `pnpm --filter @paperclipai/adapter-cursor-local typecheck` passed. - `pnpm -r typecheck` passed. - All GitHub CI checks passed on the PR head. One preview-runtime readiness test failed on the first attempt; its full local suite passed (28 tests, three skips) and the failed CI shard passed on retry. No unrelated source change was needed. - The broad local `pnpm test:run` command did not complete in the available verification window and was stopped; no full local-suite pass is claimed. The full sharded GitHub CI suite passed. `pnpm build` passed. - Tests use local fixture processes. They make no Cursor provider requests. ## Risks A future Cursor notice format may no longer match and will remain visible. Retrieval error lines and unknown diagnostics remain visible. This improves diagnosis; it does not claim to fix an unknown provider or machine failure. There are no schema, authentication, cancellation, or retry-policy changes. ## Model Used OpenAI Codex (GPT-6), with reasoning, repository inspection, and command execution. The session does not expose a more specific model revision or context-window size. ## 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 described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run the targeted tests locally and they pass; full checks are in progress - [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 - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3b34d45a92
commit
35a4448c02
7 files changed
+107
-22
No files matched your search
@@ -137,3 +137,12 @@ Rough tiers, richest first:
|
||||
## UI Parser Contract
|
||||
|
||||
External adapters can ship a self-contained UI parser that tells the Paperclip web UI how to render their stdout. Without it, the UI uses a generic shell parser. See the [UI Parser Contract](/adapters/adapter-ui-parser) for details.
|
||||
|
||||
### Cursor failure details
|
||||
|
||||
The Cursor CLI adapter uses structured error output first, then the first
|
||||
nonempty diagnostic line. It skips the informational `cursor-retrieval: tracing
|
||||
to ...` file-location notice. If the CLI exits unsuccessfully with only that
|
||||
notice, the run shows the exit code. The original stdout and stderr remain in
|
||||
the local run result and log for troubleshooting. Environment probes use the
|
||||
same diagnostic selection. This does not change retry or success decisions.
|
||||
@@ -138,6 +138,43 @@ function createFreshLeaseSandboxRunner(options: {
|
||||
}
|
||||
|
||||
describe("cursor execute", () => {
|
||||
it.each([
|
||||
{ detail: "Authentication failed", structured: "", expected: "Authentication failed" },
|
||||
{ detail: "", structured: "", expected: "Cursor exited with code 7" },
|
||||
{ detail: "stderr fallback", structured: '{"type":"error","message":"Structured failure"}', expected: "Structured failure" },
|
||||
])("keeps the actual failure after a retrieval trace announcement: $expected", async ({ detail, structured, expected }) => {
|
||||
setPrepareCursorSandboxCommand.mockReset();
|
||||
setPrepareCursorSandboxCommand.mockImplementation(async (input) => ({
|
||||
command: input.command, env: input.env, remoteSystemHomeDir: null,
|
||||
addedPathEntry: null, preferredCommandPath: null,
|
||||
}));
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-diagnostic-"));
|
||||
const command = path.join(root, "agent.sh");
|
||||
const trace = "cursor-retrieval: tracing to '/tmp/fixture-cursor-retrieval.log'";
|
||||
// Values are fixed test fixtures, passed through env rather than shell code.
|
||||
await fs.writeFile(command, `#!/bin/sh
|
||||
cat >/dev/null
|
||||
printf '%s\\n' "$FIXTURE_TRACE" "$FIXTURE_DETAIL" >&2
|
||||
printf '%s\\n' "$FIXTURE_STRUCTURED"
|
||||
exit 7
|
||||
`, { mode: 0o755 });
|
||||
try {
|
||||
const result = await execute({
|
||||
runId: "run-diagnostic-1",
|
||||
agent: { id: "agent-1", companyId: "company-1", name: "Cursor", adapterType: "cursor", adapterConfig: {} },
|
||||
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
|
||||
config: { command, cwd: root, env: { FIXTURE_TRACE: trace, FIXTURE_DETAIL: detail, FIXTURE_STRUCTURED: structured } },
|
||||
context: createPromptContextFixture(), authToken: "fixture-run-token", onLog: async () => {},
|
||||
});
|
||||
expect(result.exitCode).toBe(7);
|
||||
expect(result.errorMessage).toBe(expected);
|
||||
expect(result.resultJson?.stderr).toContain(trace);
|
||||
expect(result.resultJson?.stderr).toContain(detail);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("installs the default agent command on a fresh sandbox lease before execution", async () => {
|
||||
setPrepareCursorSandboxCommand.mockReset();
|
||||
setPrepareCursorSandboxCommand.mockImplementation(async (input) => {
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
joinPromptSections,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
|
||||
import { firstCursorDiagnosticLine, parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
|
||||
import { prepareCursorSandboxCommand } from "./remote-command.js";
|
||||
import { normalizeCursorStreamLine } from "../shared/stream.js";
|
||||
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
||||
@@ -60,15 +60,6 @@ import { resolveCursorSkillsHome } from "./skills.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function firstNonEmptyLine(text: string): string {
|
||||
return (
|
||||
text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean) ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||
const raw = env[key];
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
@@ -726,7 +717,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
} as Record<string, unknown>)
|
||||
: null;
|
||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||
const stderrLine = firstCursorDiagnosticLine(attempt.proc.stderr);
|
||||
const fallbackErrorMessage =
|
||||
parsedError ||
|
||||
stderrLine ||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { firstCursorDiagnosticLine } from "./parse.js";
|
||||
|
||||
describe("Cursor diagnostic selection", () => {
|
||||
it("skips trace announcements with CRLF, whitespace and ANSI color", () => {
|
||||
expect(firstCursorDiagnosticLine("\r\n\x1b[90m cursor-retrieval: tracing to '/tmp/fixture.log' \x1b[0m\r\ncursor-retrieval: tracing to \"/tmp/second.log\"\r\nAuthentication failed\r\nmore detail"))
|
||||
.toBe("Authentication failed");
|
||||
});
|
||||
|
||||
it("keeps retrieval failures and unknown diagnostic formats", () => {
|
||||
for (const line of [
|
||||
"cursor-retrieval: failed to open trace file",
|
||||
"cursor-retrieval: tracing to '/tmp/fixture.log' failed: permission denied",
|
||||
"unknown warning: preserve this diagnostic",
|
||||
]) expect(firstCursorDiagnosticLine(line)).toBe(line);
|
||||
});
|
||||
|
||||
it("leaves no diagnostic when only a trace location was printed", () => {
|
||||
expect(firstCursorDiagnosticLine("cursor-retrieval: tracing to '/tmp/fixture.log'\n")).toBe("");
|
||||
expect(firstCursorDiagnosticLine("\n\r\n")).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,17 @@
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
import { asString, asNumber, parseObject, parseJson } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { normalizeCursorStreamLine } from "../shared/stream.js";
|
||||
|
||||
/** Select diagnostics without mistaking Cursor's trace-file notice for an error. */
|
||||
export function firstCursorDiagnosticLine(text: string): string {
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = stripVTControlCharacters(raw).trim();
|
||||
if (!line || /^cursor-retrieval: tracing to (?:'[^']*'|"[^"]*")$/.test(line)) continue;
|
||||
return line;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function asErrorText(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
const rec = parseObject(value);
|
||||
|
||||
@@ -70,6 +70,30 @@ function createSandboxRunner(options: { homeDir: string; installCommandPath: str
|
||||
}
|
||||
|
||||
describe("cursor testEnvironment", () => {
|
||||
it("shows the probe failure after an informational retrieval trace notice", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-probe-diagnostic-"));
|
||||
const command = path.join(root, "agent");
|
||||
await fs.writeFile(command, `#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then
|
||||
printf '%s\\n' 'Cursor Agent 1.2.3'
|
||||
exit 0
|
||||
fi
|
||||
printf '%s\\n' "cursor-retrieval: tracing to '/tmp/fixture.log'" 'Provider connection failed' >&2
|
||||
exit 7
|
||||
`, { mode: 0o755 });
|
||||
try {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1", adapterType: "cursor",
|
||||
config: { command: "agent", cwd: root, env: { PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, CURSOR_API_KEY: "fixture-cursor-key" } },
|
||||
});
|
||||
expect(result.checks).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ code: "cursor_hello_probe_failed", detail: "Provider connection failed" }),
|
||||
]));
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("re-resolves the installed agent under ~/.cursor/bin and verifies --version before the hello probe", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-envtest-"));
|
||||
const homeDir = path.join(root, "home");
|
||||
|
||||
@@ -22,7 +22,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { parseCursorJsonl } from "./parse.js";
|
||||
import { firstCursorDiagnosticLine, parseCursorJsonl } from "./parse.js";
|
||||
import { isDefaultCursorCommand, prepareCursorSandboxCommand } from "./remote-command.js";
|
||||
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
||||
|
||||
@@ -36,17 +36,8 @@ function isNonEmpty(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function firstNonEmptyLine(text: string): string {
|
||||
return (
|
||||
text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean) ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeProbeDetail(stdout: string, stderr: string, parsedError: string | null): string | null {
|
||||
const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout);
|
||||
const raw = parsedError?.trim() || firstCursorDiagnosticLine(stderr) || firstCursorDiagnosticLine(stdout);
|
||||
if (!raw) return null;
|
||||
const clean = raw.replace(/\s+/g, " ").trim();
|
||||
const max = 240;
|
||||
|
||||
Reference in new issue
Block a user