mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(cursor): persist resume id early and return 409 for resume_unavailable (#745)
Remote cursor launcher now mirrors local launcher by writing cursorSessionId to hub metadata as soon as --resume is known, before the agent init event. POST /sessions/:id/resume maps resume_unavailable to 409 with clearer guidance. Fixes tiann/hapi#744 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -82,6 +82,9 @@ class CursorRemoteLauncher extends RemoteLauncherBase {
|
||||
};
|
||||
|
||||
let cursorSessionId: string | null = session.sessionId;
|
||||
if (cursorSessionId) {
|
||||
session.onSessionFound(cursorSessionId);
|
||||
}
|
||||
|
||||
while (!this.shouldExit) {
|
||||
const waitSignal = this.abortController.signal;
|
||||
|
||||
@@ -818,7 +818,7 @@ describe('session model', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'error',
|
||||
message: 'Resume session ID unavailable',
|
||||
message: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
})
|
||||
} finally {
|
||||
@@ -1011,7 +1011,39 @@ describe('session model', () => {
|
||||
|
||||
expect(engine.resolveLocalResumeTarget(session.id, 'default')).toEqual({
|
||||
type: 'error',
|
||||
message: 'Resume session ID unavailable',
|
||||
message: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
})
|
||||
} finally {
|
||||
engine.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('returns resume_unavailable when a cursor session lacks cursorSessionId', () => {
|
||||
const store = new Store(':memory:')
|
||||
const engine = new SyncEngine(
|
||||
store,
|
||||
{} as never,
|
||||
new RpcRegistry(),
|
||||
{ broadcast() {} } as never
|
||||
)
|
||||
|
||||
try {
|
||||
const session = engine.getOrCreateSession(
|
||||
'local-resume-cursor-no-id',
|
||||
{
|
||||
path: '/tmp/project',
|
||||
host: 'localhost',
|
||||
machineId: 'machine-1',
|
||||
flavor: 'cursor'
|
||||
},
|
||||
null,
|
||||
'default'
|
||||
)
|
||||
|
||||
expect(engine.resolveLocalResumeTarget(session.id, 'default')).toEqual({
|
||||
type: 'error',
|
||||
message: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -540,7 +540,11 @@ export class SyncEngine {
|
||||
|
||||
const agentSessionId = this.resolveAgentResumeId(session, namespace)
|
||||
if (!agentSessionId) {
|
||||
return { type: 'error', message: 'Resume session ID unavailable', code: 'resume_unavailable' }
|
||||
return {
|
||||
type: 'error',
|
||||
message: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -569,6 +569,32 @@ describe('sessions routes', () => {
|
||||
expect(capturedResumeOpts).toEqual({ permissionMode: 'bypassPermissions' })
|
||||
})
|
||||
|
||||
it('returns 409 when resume token is unavailable', async () => {
|
||||
const session = createSession({
|
||||
active: false,
|
||||
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'cursor' }
|
||||
})
|
||||
const { app } = createApp(session, {
|
||||
resumeSession: async () => ({
|
||||
type: 'error',
|
||||
message: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
})
|
||||
})
|
||||
|
||||
const response = await app.request('/api/sessions/session-1/resume', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(await response.json()).toEqual({
|
||||
error: 'Resume session ID unavailable. Start a new session in this directory, or retry after the agent has initialized.',
|
||||
code: 'resume_unavailable'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to metadata slash commands when RPC listing fails', async () => {
|
||||
const session = createSession({
|
||||
metadata: {
|
||||
|
||||
@@ -140,7 +140,8 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
const status = result.code === 'no_machine_online' ? 503
|
||||
: result.code === 'access_denied' ? 403
|
||||
: result.code === 'session_not_found' ? 404
|
||||
: 500
|
||||
: result.code === 'resume_unavailable' ? 409
|
||||
: 500
|
||||
return c.json({ error: result.message, code: result.code }, status)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user