mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-21 21:29:47 +00:00
fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds (closes #939) (#948)
* fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds Emit session-ready from the CLI after ACP load/newSession completes; hub resumeSession and cursor dedup wait for that signal before merging rows so a failed session/load no longer deletes the archived session the operator can retry. Refs #917. Closes #939. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): gate session-ready wait on cursor ACP protocol only Legacy stream-json Cursor resumes use cursorLegacyRemoteLauncher, which does not emit session-ready; limiting the defer-merge and dedup gates to ACP avoids 60s resume_failed timeouts on those sessions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): block ACP dedup until session-ready, including on session-end Inactive ACP spawns that never emitted session-ready could still trigger deduplicateByAgentSessionId on session-end and delete the original row. Require session-ready for all ACP dedup paths and skip end-of-session dedup when load never succeeded. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): restore session-end dedup for non-ACP cursor duplicates Only skip the session-end dedup retry for Cursor ACP rows that never emitted session-ready. Codex/Claude/legacy Cursor duplicates still merge when the live row ends. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -138,15 +138,7 @@ import { ApiSessionClient } from '@/api/apiSession';
|
||||
|
||||
function makeSession(sessionId: string | null): CursorSession {
|
||||
const queue = new MessageQueue2<EnhancedMode>(() => 'mode');
|
||||
const client = {
|
||||
rpcHandlerManager: {
|
||||
registerHandler: vi.fn()
|
||||
},
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
const client = makeClient();
|
||||
|
||||
const session = new CursorSession({
|
||||
api: {} as never,
|
||||
@@ -168,6 +160,19 @@ function makeSession(sessionId: string | null): CursorSession {
|
||||
return session;
|
||||
}
|
||||
|
||||
function makeClient() {
|
||||
return {
|
||||
rpcHandlerManager: {
|
||||
registerHandler: vi.fn()
|
||||
},
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
}
|
||||
|
||||
describe('cursorAcpRemoteLauncher', () => {
|
||||
beforeEach(() => {
|
||||
harness.initializeError = null;
|
||||
@@ -265,6 +270,26 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
expect(harness.newSessionCalled).toBe(true);
|
||||
expect(harness.loadSessionCalled).toBe(false);
|
||||
expect(session.onSessionFoundWithProtocol).toHaveBeenCalledWith('new-acp-session', 'acp');
|
||||
expect(session.client.emitSessionReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits session-ready after session/load succeeds', async () => {
|
||||
const session = makeSession('resume-thread-ready');
|
||||
await cursorAcpRemoteLauncher(session);
|
||||
|
||||
expect(harness.loadSessionCalled).toBe(true);
|
||||
expect(session.client.emitSessionReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not emit session-ready when session/load fails', async () => {
|
||||
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/
|
||||
);
|
||||
|
||||
expect(session.client.emitSessionReady).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies debug mode immediately when setPermissionMode is called', async () => {
|
||||
@@ -274,7 +299,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -318,7 +344,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive
|
||||
keepAlive,
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -362,7 +389,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive
|
||||
keepAlive,
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -409,7 +437,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -453,7 +482,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -509,7 +539,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive
|
||||
keepAlive,
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -550,7 +581,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -595,7 +627,8 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
updateMetadata: vi.fn(),
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn()
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
const session = new CursorSession({
|
||||
@@ -636,6 +669,7 @@ describe('cursorAcpRemoteLauncher', () => {
|
||||
sendSessionEvent: vi.fn(),
|
||||
sendAgentMessage: vi.fn(),
|
||||
keepAlive: vi.fn(),
|
||||
emitSessionReady: vi.fn(),
|
||||
emitMessagesConsumed: vi.fn()
|
||||
} as unknown as ApiSessionClient;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user