fix(mobile): sequence structured hold before subscribe

This commit is contained in:
Merge Sim
2026-09-01 14:27:01 -07:00
parent f8f5e24033
commit 7c4f94b0e4
2 changed files with 76 additions and 34 deletions
@@ -254,6 +254,34 @@ describe('useMobileStructuredAgentSession', () => {
)
})
it('waits for the structured hold before subscribing', async () => {
let resolveHold: (() => void) | null = null
sendRequest.mockImplementation((method, params) => {
if (method === 'agentSession.hold') {
return new Promise((resolve) => {
resolveHold = () => resolve(ok({ held: true }))
})
}
return defaultSendRequest(method, params)
})
act(() => {
renderer = create(createElement(Harness))
})
await vi.waitFor(() => expect(resolveHold).not.toBeNull())
expect(subscribe).not.toHaveBeenCalled()
await act(async () => resolveHold?.())
await vi.waitFor(() =>
expect(subscribe).toHaveBeenCalledWith(
'agentSession.subscribe',
{ sessionId: 'session-1' },
expect.any(Function)
)
)
})
it('sends with the shared structured mutation envelope after the stream fence lands', async () => {
act(() => {
renderer = create(createElement(Harness))
@@ -45,31 +45,6 @@ export function useMobileStructuredAgentState(args: {
setState((current) => reduceStructuredAgentSession(current, action))
}, [])
useEffect(() => {
if (!client || !sessionId || !enabled) {
return
}
const holderId = structuredAgentSessionHolderId('mobile-chat')
const held = callAgentSession(client, 'agentSession.hold', {
sessionId,
holderId
}).catch(() => undefined)
return () => {
void held.then(() =>
callAgentSession(
client,
'agentSession.release',
{
sessionId,
holderId
},
undefined,
{ failWhenDisconnected: true }
).catch(() => undefined)
)
}
}, [client, enabled, sessionId])
useEffect(() => {
if (!client || !sessionId || !enabled) {
setState(EMPTY_STRUCTURED_AGENT_SESSION)
@@ -77,16 +52,55 @@ export function useMobileStructuredAgentState(args: {
return
}
apply({ type: 'loading' })
const unsubscribe = client.subscribe('agentSession.subscribe', { sessionId }, (raw) => {
if (typeof raw === 'object' && raw !== null && (raw as { type?: unknown }).type === 'error') {
apply({ type: 'error', message: String((raw as { message?: unknown }).message ?? '') })
return
}
if (isSubscribeEvent(raw)) {
apply({ type: 'event', event: raw })
}
const holderId = structuredAgentSessionHolderId('mobile-chat')
let cancelled = false
let unsubscribe = (): void => {}
const held = callAgentSession(client, 'agentSession.hold', {
sessionId,
holderId
})
return unsubscribe
void held
.then(() => {
if (cancelled) {
return
}
unsubscribe = client.subscribe('agentSession.subscribe', { sessionId }, (raw) => {
if (
typeof raw === 'object' &&
raw !== null &&
(raw as { type?: unknown }).type === 'error'
) {
apply({ type: 'error', message: String((raw as { message?: unknown }).message ?? '') })
return
}
if (isSubscribeEvent(raw)) {
apply({ type: 'event', event: raw })
}
})
})
.catch((error: unknown) => {
if (!cancelled) {
apply({ type: 'error', message: error instanceof Error ? error.message : String(error) })
}
})
return () => {
cancelled = true
unsubscribe()
void held
.then(() =>
callAgentSession(
client,
'agentSession.release',
{
sessionId,
holderId
},
undefined,
{ failWhenDisconnected: true }
).catch(() => undefined)
)
.catch(() => undefined)
}
}, [apply, client, enabled, sessionId])
const loadEarlier = useCallback(() => {