diff --git a/mobile/src/session/host-session-native-chat-operations.ts b/mobile/src/session/host-session-native-chat-operations.ts index 635925405a0..fa37cb77a38 100644 --- a/mobile/src/session/host-session-native-chat-operations.ts +++ b/mobile/src/session/host-session-native-chat-operations.ts @@ -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 + /** 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, diff --git a/mobile/src/session/host-session-terminal-operations.ts b/mobile/src/session/host-session-terminal-operations.ts index 144aded7d30..5bffe947238 100644 --- a/mobile/src/session/host-session-terminal-operations.ts +++ b/mobile/src/session/host-session-terminal-operations.ts @@ -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, diff --git a/mobile/src/session/native-host-session-terminal-operations.ts b/mobile/src/session/native-host-session-terminal-operations.ts index a518aedc047..835f8156b8c 100644 --- a/mobile/src/session/native-host-session-terminal-operations.ts +++ b/mobile/src/session/native-host-session-terminal-operations.ts @@ -22,7 +22,6 @@ export function nativeHostSessionTerminalOperations( ) return unsubscribe }, - acknowledge() {}, async sendInput(terminalId, text, enter, clientId) { return client .sendRequest( diff --git a/mobile/src/session/use-mobile-native-chat-session.ts b/mobile/src/session/use-mobile-native-chat-session.ts index 86873816db7..e5de241cea8 100644 --- a/mobile/src/session/use-mobile-native-chat-session.ts +++ b/mobile/src/session/use-mobile-native-chat-session.ts @@ -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