fix(cli): surface real Cursor ACP session/load errors (#1198)

* fix(cli): surface real Cursor ACP session/load errors

Stop mislabeling every ACP session/load failure as a legacy
stream-json protocol problem. Prefer Cursor's Cannot use this model
stderr (including Available models when present), attach drained
stderr on process close, and keep structured formatAcpLoadError logs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): accumulate ACP stderr across split chunks

Address Codex Major on #1198: child_process stderr data events are not
message boundaries. Concatenate raw chunks in a rolling window, extract
Cannot use this model from the window on close, and prefer that over a
partial onStderrError hint when classifying resume failures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): pin ACP model-rejection stderr when catalog overflows

Once Cannot use this model appears, keep the buffer from that match
head so a long Available models list cannot roll the rejection out of
the rolling window (Codex follow-up on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): wait for model id before ACP model-rejection emit

Only emit Cannot use this model via onStderrError once a non-space
token follows the colon, so a split before the id cannot suppress the
completed rolling-window message (Codex Minor on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): block ACP writes between process exit and close

Keep the post-exit stdin write guard while deferring markClosed until
stdio close so stderr can still enrich the failure (Codex Minor on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-07-29 10:05:41 +08:00
committed by GitHub
co-authored by Cursor
parent 4c203f17cb
commit d8aa8736cd
4 changed files with 494 additions and 20 deletions
+77 -3
View File
@@ -91,8 +91,10 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
backend.setUsageUpdateListener((message) => this.handleAgentMessage(message));
let recentStderrHint: string | null = null;
backend.onStderrError((error) => {
logger.debug('[cursor-acp] stderr error', error);
recentStderrHint = error.raw || error.message;
const converted = convertAgentMessage({ type: 'error', message: error.message });
if (converted) {
session.sendAgentMessage(converted);
@@ -104,6 +106,20 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
await backend.initialize();
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
const modelRejection = extractCannotUseThisModelMessage(errMsg)
?? extractCannotUseThisModelMessage(recentStderrHint);
if (modelRejection) {
const fullMsg = classifyCursorAcpLoadError(error, {
recentStderr: recentStderrHint,
action: 'start'
});
const converted = convertAgentMessage({ type: 'error', message: fullMsg });
if (converted) {
session.sendAgentMessage(converted);
}
messageBuffer.addMessage(fullMsg, 'status');
throw new Error(fullMsg);
}
const fullMsg = `${CURSOR_ACP_REQUIRED_MESSAGE} (${errMsg})`;
const converted = convertAgentMessage({ type: 'error', message: fullMsg });
if (converted) {
@@ -145,9 +161,7 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
});
} catch (error) {
logger.warn('[cursor-acp] session/load failed', formatAcpLoadError(error));
throw new Error(
'Failed to resume Cursor ACP session. Legacy stream-json sessions cannot be loaded via ACP.'
);
throw new Error(classifyCursorAcpLoadError(error, { recentStderr: recentStderrHint }));
}
} else if (resumeSessionId) {
throw new Error(
@@ -578,6 +592,62 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
}
}
const CANNOT_USE_THIS_MODEL_RE = /Cannot use this model:\s*.+/i;
/**
* Operator-facing ACP failure text. Prefer Cursor's model-rejection stderr;
* never invent a legacy stream-json diagnosis for unrelated failures.
*/
export function classifyCursorAcpLoadError(
error: unknown,
options?: { recentStderr?: string | null; action?: 'resume' | 'start' }
): string {
const action = options?.action ?? 'resume';
const prefix = action === 'start'
? 'Failed to start Cursor ACP session'
: 'Failed to resume Cursor ACP session';
const detailSources = [
// Prefer the close Error (accumulated stderr) over live onStderrError hints,
// which may have seen only the first fragment of a split rejection line.
error instanceof Error ? error.message : null,
error instanceof Error ? String((error as Error & { stderr?: unknown }).stderr ?? '') : null,
error instanceof Error && error.cause instanceof Error ? error.cause.message : null,
options?.recentStderr,
typeof error === 'string' ? error : null
].filter((value): value is string => Boolean(value && value.trim()));
for (const source of detailSources) {
const modelRejection = extractCannotUseThisModelMessage(source);
if (modelRejection) {
return `${prefix}: ${modelRejection}`;
}
}
const detail = error instanceof Error
? error.message
: typeof error === 'string'
? error
: String(error);
const trimmed = detail.trim() || 'unknown error';
if (new RegExp(`^${prefix}:`, 'i').test(trimmed)) {
return trimmed;
}
return `${prefix}: ${trimmed}`;
}
function extractCannotUseThisModelMessage(text: string | null | undefined): string | null {
if (!text) {
return null;
}
const match = text.match(CANNOT_USE_THIS_MODEL_RE);
if (!match) {
return null;
}
// Keep Cursor's Available models hint when present; do not invent a catalog.
return match[0].trim().replace(/\s+/g, ' ');
}
function formatAcpLoadError(error: unknown): Record<string, unknown> {
if (error instanceof Error) {
const record: Record<string, unknown> = {
@@ -592,6 +662,10 @@ function formatAcpLoadError(error: unknown): Record<string, unknown> {
if (data !== undefined) {
record.data = data;
}
const stderr = (error as Error & { stderr?: unknown }).stderr;
if (stderr !== undefined) {
record.stderr = stderr;
}
const cause = error.cause;
if (cause !== undefined) {
record.cause = cause instanceof Error