fix(mobile): remove a stream error callback that could never fire

The native chat subscribe declared an onError the adapter never invoked, since
the transport has none: a stream error arrives as an error frame through the
listener, which is where the caller already handles it. The hook's second
callback was unreachable, so it goes with the parameter.

The terminal adapter's acknowledge was a no-op with no caller here or on main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-09 01:45:44 -04:00
parent e1b7aa8223
commit cb10440dca
4 changed files with 57 additions and 68 deletions
@@ -36,11 +36,12 @@ export type HostSessionNativeChatImageAttachResult =
export type HostSessionNativeChatOperations = {
/** Whether the serving host can read this workspace's agent transcripts. */
readability(workspaceId: string): Promise<boolean>
/** No error callback: the transport has none, and a stream error arrives as an `error` frame
* through `onEvent`, which is where the caller already handles it. */
subscribe(
target: HostSessionNativeChatTarget,
limit: number,
onEvent: (event: MobileNativeChatStreamFrame) => void,
onError: () => void
onEvent: (event: MobileNativeChatStreamFrame) => void
): () => void
read(
target: HostSessionNativeChatTarget,
@@ -57,7 +57,6 @@ export type HostSessionTerminalOperations = {
onEvent: (event: HostSessionTerminalStreamEvent) => void,
onError: () => void
): () => void
acknowledge(terminalId: string, throughSequence: number): void
sendInput(
terminalId: string,
text: string,
@@ -22,7 +22,6 @@ export function nativeHostSessionTerminalOperations(
)
return unsubscribe
},
acknowledge() {},
async sendInput(terminalId, text, enter, clientId) {
return client
.sendRequest(
@@ -156,71 +156,61 @@ export function useMobileNativeChatSession(args: {
sessionId,
transcriptPath
})
const unsubscribe = operations.subscribe(
target,
limitRef.current,
(raw) => {
if (cancelled) {
return
}
const frame = raw as MobileNativeChatStreamFrame
const applied = applyMobileNativeChatStreamFrame({
merger: mergerRef.current,
frame,
limit: limitRef.current,
replaceSnapshot: !snapshotSeenRef.current
})
if (applied.kind === 'ignored') {
return
}
if (applied.kind === 'error') {
setRead({ operations, identity, status: 'error' })
setError(applied.error)
return
}
if (frame.type === 'snapshot' && !applied.pending) {
// A pending window has no transcript behind it, so the snapshot that
// follows is still this subscription's base, not a reconnect replay.
snapshotSeenRef.current = true
}
if (applied.windowReplaced || frame.type === 'snapshot') {
// Why: any authoritative window (and any replay merge) invalidates an
// in-flight older-page request; stale results must not land on it.
streamGenerationRef.current += 1
loadingEarlierRef.current = false
setLoadingEarlier(false)
}
if (applied.windowReplaced) {
// Only a genuinely fresh window resets the grown read window — an
// overlapping reconnect replay keeps the paged-in history and limit.
limitRef.current = INITIAL_LIMIT
beforeOffsetRef.current = applied.beforeOffset ?? null
setHasMore(applied.hasMore ?? applied.messages.length >= INITIAL_LIMIT)
}
setMessages(applied.messages)
if (!applied.windowReplaced && applied.hasMore != null) {
setHasMore(applied.hasMore)
}
if (!applied.windowReplaced && applied.beforeOffset != null) {
beforeOffsetRef.current = applied.beforeOffset
}
if (applied.cursorInvalidated) {
// Fall back to a growing-tail read so history trimmed by live appends
// cannot leave a gap between the retained window and the old cursor.
streamGenerationRef.current += 1
loadingEarlierRef.current = false
setLoadingEarlier(false)
beforeOffsetRef.current = null
}
setRead({ operations, identity, status: applied.pending ? 'awaiting-transcript' : 'ready' })
},
() => {
if (!cancelled) {
setRead({ operations, identity, status: 'error' })
setError('Transcript stream failed')
}
const unsubscribe = operations.subscribe(target, limitRef.current, (raw) => {
if (cancelled) {
return
}
)
const frame = raw as MobileNativeChatStreamFrame
const applied = applyMobileNativeChatStreamFrame({
merger: mergerRef.current,
frame,
limit: limitRef.current,
replaceSnapshot: !snapshotSeenRef.current
})
if (applied.kind === 'ignored') {
return
}
if (applied.kind === 'error') {
setRead({ operations, identity, status: 'error' })
setError(applied.error)
return
}
if (frame.type === 'snapshot' && !applied.pending) {
// A pending window has no transcript behind it, so the snapshot that
// follows is still this subscription's base, not a reconnect replay.
snapshotSeenRef.current = true
}
if (applied.windowReplaced || frame.type === 'snapshot') {
// Why: any authoritative window (and any replay merge) invalidates an
// in-flight older-page request; stale results must not land on it.
streamGenerationRef.current += 1
loadingEarlierRef.current = false
setLoadingEarlier(false)
}
if (applied.windowReplaced) {
// Only a genuinely fresh window resets the grown read window — an
// overlapping reconnect replay keeps the paged-in history and limit.
limitRef.current = INITIAL_LIMIT
beforeOffsetRef.current = applied.beforeOffset ?? null
setHasMore(applied.hasMore ?? applied.messages.length >= INITIAL_LIMIT)
}
setMessages(applied.messages)
if (!applied.windowReplaced && applied.hasMore != null) {
setHasMore(applied.hasMore)
}
if (!applied.windowReplaced && applied.beforeOffset != null) {
beforeOffsetRef.current = applied.beforeOffset
}
if (applied.cursorInvalidated) {
// Fall back to a growing-tail read so history trimmed by live appends
// cannot leave a gap between the retained window and the old cursor.
streamGenerationRef.current += 1
loadingEarlierRef.current = false
setLoadingEarlier(false)
beforeOffsetRef.current = null
}
setRead({ operations, identity, status: applied.pending ? 'awaiting-transcript' : 'ready' })
})
return () => {
cancelled = true