mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-05 01:01:42 +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:
@@ -135,7 +135,7 @@ vi.mock('@/ui/logger', () => ({
|
||||
logger: { debug: vi.fn(), warn: vi.fn(), info: vi.fn() }
|
||||
}));
|
||||
|
||||
import { cursorAcpRemoteLauncher } from './cursorAcpRemoteLauncher';
|
||||
import { classifyCursorAcpLoadError, cursorAcpRemoteLauncher } from './cursorAcpRemoteLauncher';
|
||||
import { createCursorAcpBackend } from './utils/cursorAcpBackend';
|
||||
import { CursorSession } from './session';
|
||||
import { ApiSessionClient } from '@/api/apiSession';
|
||||
@@ -218,13 +218,28 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
await expect(cursorAcpRemoteLauncher(session)).rejects.toThrow(
|
||||
/Cursor ACP mode is required for new Cursor remote sessions/
|
||||
);
|
||||
|
||||
expect(client.sendAgentMessage).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: expect.stringContaining('agent acp not found')
|
||||
});
|
||||
expect(legacyLauncher).not.toHaveBeenCalled();
|
||||
expect(harness.newSessionCalled).toBe(false);
|
||||
expect(client.sendAgentMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces Cursor model rejection during initialize instead of the generic ACP-required message', async () => {
|
||||
harness.initializeError = new Error(
|
||||
'ACP process exited (code=1, signal=null). stderr: Cannot use this model: grok-4.5[fast=true]. Available models: auto'
|
||||
);
|
||||
const session = makeSession(null);
|
||||
|
||||
const error = await cursorAcpRemoteLauncher(session).then(
|
||||
() => null,
|
||||
(err: unknown) => err
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toMatch(
|
||||
/^Failed to start Cursor ACP session: Cannot use this model: grok-4\.5\[fast=true\]/
|
||||
);
|
||||
expect((error as Error).message).toMatch(/Available models: auto/);
|
||||
expect((error as Error).message).not.toMatch(/Cursor ACP mode is required/);
|
||||
expect((error as Error).message).not.toMatch(/Legacy stream-json/);
|
||||
expect(legacyLauncher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers cursorSessionId before session/load completes', async () => {
|
||||
@@ -252,15 +267,38 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
harness.loadSessionError = new Error('session not found');
|
||||
const session = makeSession('old-stream-json-id');
|
||||
|
||||
await expect(cursorAcpRemoteLauncher(session)).rejects.toThrow(
|
||||
/Legacy stream-json sessions cannot be loaded via ACP/
|
||||
const error = await cursorAcpRemoteLauncher(session).then(
|
||||
() => null,
|
||||
(err: unknown) => err
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toMatch(/Failed to resume Cursor ACP session: session not found/);
|
||||
expect((error as Error).message).not.toMatch(/Legacy stream-json/);
|
||||
|
||||
expect(harness.loadSessionCalled).toBe(true);
|
||||
expect(harness.newSessionCalled).toBe(false);
|
||||
expect(legacyLauncher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces Cursor model rejection from session/load instead of claiming legacy protocol', async () => {
|
||||
harness.loadSessionError = new Error(
|
||||
'ACP process exited (code=1, signal=null). stderr: Cannot use this model: grok-4.5[fast=true]. Available models: auto, cursor-grok-4.5-high-fast'
|
||||
);
|
||||
const session = makeSession('acp-thread-1');
|
||||
|
||||
const error = await cursorAcpRemoteLauncher(session).then(
|
||||
() => null,
|
||||
(err: unknown) => err
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toMatch(/Cannot use this model: grok-4\.5\[fast=true\]/);
|
||||
expect((error as Error).message).toMatch(/Available models: auto, cursor-grok-4\.5-high-fast/);
|
||||
expect((error as Error).message).not.toMatch(/Legacy stream-json/);
|
||||
|
||||
expect(harness.newSessionCalled).toBe(false);
|
||||
expect(legacyLauncher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when resume id is set but session/load is unsupported', async () => {
|
||||
harness.supportsLoadSession = false;
|
||||
const session = makeSession('some-session-id');
|
||||
@@ -297,12 +335,63 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
const session = makeSession('old-stream-json-id');
|
||||
|
||||
await expect(cursorAcpRemoteLauncher(session)).rejects.toThrow(
|
||||
/Legacy stream-json sessions cannot be loaded via ACP/
|
||||
/Failed to resume Cursor ACP session: session not found/
|
||||
);
|
||||
|
||||
expect(session.client.emitSessionReady).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('classifyCursorAcpLoadError', () => {
|
||||
it('prefers Cannot use this model text from the underlying error', () => {
|
||||
const message = classifyCursorAcpLoadError(
|
||||
new Error('ACP process exited (code=1, signal=null). stderr: Cannot use this model: grok-4.5[fast=true]. Available models: auto, composer-2.5')
|
||||
);
|
||||
expect(message).toContain('Cannot use this model: grok-4.5[fast=true]');
|
||||
expect(message).toContain('Available models: auto, composer-2.5');
|
||||
expect(message).not.toMatch(/Legacy stream-json/);
|
||||
});
|
||||
|
||||
it('uses recentStderr hint when exit error omits the model line', () => {
|
||||
const message = classifyCursorAcpLoadError(
|
||||
new Error('ACP process exited (code=1, signal=null)'),
|
||||
{ recentStderr: 'Cannot use this model: stale-id. Available models: auto' }
|
||||
);
|
||||
expect(message).toContain('Cannot use this model: stale-id');
|
||||
expect(message).toContain('Available models: auto');
|
||||
expect(message).not.toMatch(/Legacy stream-json/);
|
||||
});
|
||||
|
||||
it('prefers accumulated close stderr over a partial recentStderr hint', () => {
|
||||
const message = classifyCursorAcpLoadError(
|
||||
new Error(
|
||||
'ACP process exited (code=1, signal=null). stderr: Cannot use this model: full-id. Available models: auto, composer-2.5'
|
||||
),
|
||||
{ recentStderr: 'Cannot use this mo' }
|
||||
);
|
||||
expect(message).toContain('Cannot use this model: full-id');
|
||||
expect(message).toContain('Available models: auto, composer-2.5');
|
||||
expect(message).not.toContain('Cannot use this mo:');
|
||||
});
|
||||
|
||||
it('propagates generic load failures without inventing a legacy diagnosis', () => {
|
||||
const message = classifyCursorAcpLoadError(new Error('Session "abc" not found'));
|
||||
expect(message).toBe('Failed to resume Cursor ACP session: Session "abc" not found');
|
||||
expect(message).not.toMatch(/Legacy stream-json/);
|
||||
});
|
||||
|
||||
it('uses start action prefix for spawn-time model rejection', () => {
|
||||
const message = classifyCursorAcpLoadError(
|
||||
new Error('ACP process exited (code=1, signal=null)'),
|
||||
{
|
||||
recentStderr: 'Cannot use this model: stale-id. Available models: auto',
|
||||
action: 'start'
|
||||
}
|
||||
);
|
||||
expect(message).toMatch(/^Failed to start Cursor ACP session: Cannot use this model: stale-id/);
|
||||
expect(message).not.toMatch(/Failed to resume/);
|
||||
});
|
||||
});
|
||||
|
||||
// tiann/hapi#913: fresh ACP sessions previously persisted `cursorSessionId`
|
||||
// via fire-and-forget `updateMetadata`. A SIGTERM within ~1s of the first
|
||||
// turn (hub-restart cascade) could strand the session because the ACK
|
||||
|
||||
Reference in New Issue
Block a user