fix(cli,hub): wire Cursor --existing-session-id for ACP remote resume (#991) (#1128)

Hub already passes access.sessionId on resume (#1088); Cursor CLI still ignored
it (Codex-only). Parse/pass the flag for cursor and lock in reuse-without-ready-wait tests.

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-07-24 10:52:55 +08:00
committed by GitHub
co-authored by Debian Cursor
parent fee853766a
commit 40314237ae
4 changed files with 176 additions and 2 deletions
+152
View File
@@ -1240,6 +1240,158 @@ describe('session model', () => {
}
})
it('cursor ACP resume passes existingSessionId and reuses row without session-ready wait (#991)', async () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const oldSession = engine.getOrCreateSession(
'cursor-reopen-old',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'cursor',
cursorSessionId: 'cursor-csid-load-fail',
cursorSessionProtocol: 'acp'
},
null,
'default'
)
engine.getOrCreateMachine(
'machine-1',
{ host: 'localhost', platform: 'linux', happyCliVersion: '0.1.0' },
null,
'default'
)
engine.handleMachineAlive({ machineId: 'machine-1', time: Date.now() })
engine.handleSessionEnd({ sid: oldSession.id, time: Date.now() })
let capturedExistingSessionId: string | undefined
let waitForSessionReadyCalls = 0
let mergeCalls = 0
const sessionCache = (engine as any).sessionCache
const mergeSessions = sessionCache.mergeSessions.bind(sessionCache)
sessionCache.mergeSessions = async (oldSessionId: string, newSessionId: string, namespace: string) => {
mergeCalls += 1
return mergeSessions(oldSessionId, newSessionId, namespace)
}
;(engine as any).rpcGateway.spawnSession = async (
_machineId: string,
_directory: string,
_agent: string,
_model?: string,
_modelReasoningEffort?: string,
_yolo?: boolean,
_sessionType?: string,
_worktreeName?: string,
_resumeSessionId?: string,
_effort?: string,
_permissionMode?: string,
_serviceTier?: string,
existingSessionId?: string
) => {
capturedExistingSessionId = existingSessionId
engine.handleSessionAlive({ sid: oldSession.id, time: Date.now() })
return { type: 'success', sessionId: oldSession.id }
}
;(engine as any).rpcGateway.getCursorChatStoreStatus = async () => ({ onDisk: true, store: 'acp' })
;(engine as any).waitForSessionActive = async () => true
;(engine as any).waitForSessionReady = async () => {
waitForSessionReadyCalls += 1
return 'timeout'
}
const result = await engine.resumeSession(oldSession.id, 'default')
expect(result).toEqual({ type: 'success', sessionId: oldSession.id })
expect(capturedExistingSessionId).toBe(oldSession.id)
expect(waitForSessionReadyCalls).toBe(0)
expect(mergeCalls).toBe(0)
expect(store.sessions.getSession(oldSession.id)).not.toBeNull()
} finally {
engine.stop()
}
})
it('cursor ACP resume succeeds on same row without merge (#991)', async () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const oldSession = engine.getOrCreateSession(
'cursor-reopen-old-ready',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'cursor',
cursorSessionId: 'cursor-csid-load-ok',
cursorSessionProtocol: 'acp'
},
null,
'default'
)
engine.getOrCreateMachine(
'machine-1',
{ host: 'localhost', platform: 'linux', happyCliVersion: '0.1.0' },
null,
'default'
)
engine.handleMachineAlive({ machineId: 'machine-1', time: Date.now() })
engine.handleSessionEnd({ sid: oldSession.id, time: Date.now() })
let mergeCalls = 0
const sessionCache = (engine as any).sessionCache
const mergeSessions = sessionCache.mergeSessions.bind(sessionCache)
sessionCache.mergeSessions = async (oldSessionId: string, newSessionId: string, namespace: string) => {
mergeCalls += 1
return mergeSessions(oldSessionId, newSessionId, namespace)
}
;(engine as any).rpcGateway.spawnSession = async (
_machineId: string,
_directory: string,
_agent: string,
_model?: string,
_modelReasoningEffort?: string,
_yolo?: boolean,
_sessionType?: string,
_worktreeName?: string,
_resumeSessionId?: string,
_effort?: string,
_permissionMode?: string,
_serviceTier?: string,
existingSessionId?: string
) => {
expect(existingSessionId).toBe(oldSession.id)
engine.handleSessionAlive({ sid: oldSession.id, time: Date.now() })
return { type: 'success', sessionId: oldSession.id }
}
;(engine as any).rpcGateway.getCursorChatStoreStatus = async () => ({ onDisk: true, store: 'acp' })
;(engine as any).waitForSessionActive = async () => true
const result = await engine.resumeSession(oldSession.id, 'default')
expect(result).toEqual({ type: 'success', sessionId: oldSession.id })
expect(mergeCalls).toBe(0)
expect(store.sessions.getSession(oldSession.id)).not.toBeNull()
} finally {
engine.stop()
}
})
it('defers mergeSessions for cursor reopen until session-ready (load failure leaves old row)', async () => {
const store = new Store(':memory:')
const engine = new SyncEngine(