mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* fix(web): apply messages-consumed on global SSE connection The global all-sessions SSE subscription returned early on message-stream events without updating the message-window store. When session-scoped SSE was reconnecting or the user had another session selected, messages-consumed never cleared the queued bar even though the hub had stamped invoked_at. Also harden mergeMessages so a stale invokedAt:null snapshot cannot clobber an existing ack timestamp. Fixes tiann/hapi#758 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web,hub): resume never-started inactive sessions on first send Hub fresh-spawns when inactive session has path but no agent thread id and zero messages. Web guards resume, updates inactive banner copy, and surfaces resume_unavailable before POST /resume when resume is impossible. Fixes tiann/hapi#759 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): scope sessionResume guard to current flavor only Hub `resolveAgentResumeId` only honors the metadata.flavor's id; the web guard was falling back across all flavors so a cursor session with a stale codexSessionId still tried to resume and 409'd. Mirror the hub switch and default to claude when flavor is unknown. Addresses HAPI Bot review on tiann/hapi#761. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): allow claude session resume via hub message-id recovery Hub `resolveAgentResumeId` falls back to `recoverClaudeSessionIdFromMessages` on the claude branch when `metadata.claudeSessionId` is absent, so the web guard must not block inactive claude sessions that have stored messages but no metadata id. Other flavors have no such recovery path and stay rejected. Addresses second HAPI Bot review thread on tiann/hapi#761 (`web/src/lib/sessionResume.ts:41`). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { Session } from '@/types/api'
|
|
import { inactiveSessionCanResume, resolveAgentSessionIdFromMetadata } from './sessionResume'
|
|
|
|
function makeSession(overrides: Partial<Session> = {}): Session {
|
|
return {
|
|
id: 'session-1',
|
|
active: false,
|
|
thinking: false,
|
|
activeAt: 0,
|
|
updatedAt: 0,
|
|
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'cursor' },
|
|
...overrides,
|
|
} as Session
|
|
}
|
|
|
|
describe('sessionResume', () => {
|
|
it('resolveAgentSessionIdFromMetadata picks the id matching the session flavor', () => {
|
|
expect(resolveAgentSessionIdFromMetadata({
|
|
path: '/p',
|
|
host: 'h',
|
|
flavor: 'codex',
|
|
codexSessionId: 'codex-1',
|
|
cursorSessionId: 'cursor-1',
|
|
})).toBe('codex-1')
|
|
expect(resolveAgentSessionIdFromMetadata({
|
|
path: '/p',
|
|
host: 'h',
|
|
flavor: 'cursor',
|
|
cursorSessionId: 'cursor-1',
|
|
})).toBe('cursor-1')
|
|
})
|
|
|
|
it('resolveAgentSessionIdFromMetadata ignores stale cross-flavor ids', () => {
|
|
expect(resolveAgentSessionIdFromMetadata({
|
|
path: '/p',
|
|
host: 'h',
|
|
flavor: 'cursor',
|
|
codexSessionId: 'codex-1',
|
|
})).toBeUndefined()
|
|
})
|
|
|
|
it('resolveAgentSessionIdFromMetadata defaults to claude when flavor is missing', () => {
|
|
expect(resolveAgentSessionIdFromMetadata({
|
|
path: '/p',
|
|
host: 'h',
|
|
claudeSessionId: 'claude-1',
|
|
})).toBe('claude-1')
|
|
})
|
|
|
|
it('inactiveSessionCanResume is true for active sessions', () => {
|
|
expect(inactiveSessionCanResume(makeSession({ active: true }), 0)).toBe(true)
|
|
})
|
|
|
|
it('inactiveSessionCanResume allows fresh spawn when no agent id and no messages', () => {
|
|
expect(inactiveSessionCanResume(makeSession(), 0)).toBe(true)
|
|
})
|
|
|
|
it('inactiveSessionCanResume allows resume when agent id exists', () => {
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: {
|
|
path: '/tmp/project',
|
|
host: 'localhost',
|
|
flavor: 'cursor',
|
|
cursorSessionId: 'cursor-thread-1',
|
|
},
|
|
}), 5)).toBe(true)
|
|
})
|
|
|
|
it('inactiveSessionCanResume rejects inactive sessions with messages but no agent id', () => {
|
|
expect(inactiveSessionCanResume(makeSession(), 3)).toBe(false)
|
|
})
|
|
|
|
it('inactiveSessionCanResume rejects when stale cross-flavor agent id is present but no messages', () => {
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: {
|
|
path: '/tmp/project',
|
|
host: 'localhost',
|
|
flavor: 'cursor',
|
|
codexSessionId: 'stale-codex-1',
|
|
},
|
|
}), 0)).toBe(true)
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: {
|
|
path: '/tmp/project',
|
|
host: 'localhost',
|
|
flavor: 'cursor',
|
|
codexSessionId: 'stale-codex-1',
|
|
},
|
|
}), 3)).toBe(false)
|
|
})
|
|
|
|
it('inactiveSessionCanResume rejects when metadata path is missing', () => {
|
|
expect(inactiveSessionCanResume(makeSession({ metadata: { path: '', host: 'localhost' } }), 0)).toBe(false)
|
|
})
|
|
|
|
it('inactiveSessionCanResume allows claude resume by message recovery when no claudeSessionId is stored', () => {
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'claude' },
|
|
}), 3)).toBe(true)
|
|
})
|
|
|
|
it('inactiveSessionCanResume allows claude recovery when flavor is missing (defaults to claude)', () => {
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: { path: '/tmp/project', host: 'localhost' },
|
|
}), 3)).toBe(true)
|
|
})
|
|
|
|
it('inactiveSessionCanResume rejects non-claude flavors with messages but no flavor-specific id (no recovery path)', () => {
|
|
expect(inactiveSessionCanResume(makeSession({
|
|
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'codex' },
|
|
}), 3)).toBe(false)
|
|
})
|
|
})
|