mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-10 02:01:40 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user