Files
orca/src/shared/native-chat-stream-unsubscribe.ts
T
Brennan Benson 2a001b43e5 fix(mobile): stop a closing phone stream from ending the terminal or chat feed that replaced it (#22939)
* fix(mobile): don't unsubscribe a terminal stream that already ended

* fix(mobile): keep the stream registry under the line cap and expect no error after a streamed end

The streamed `end` now closes a direct stream, so a later reply for that id is unrouted instead of
reaching the listener as an error; the subscription recording test expected the old error. Inline
the error wrapper so the registry fits the 300-line cap, and shorten the comments.

* fix(mobile): reopen a native chat stream the host ended while the screen still shows it

* fix(mobile): only the focused screen takes back a host-ended chat feed, and a reopen keeps loaded history

* fix(mobile): give each native chat subscription its own token, and reopen with the first page

* test(mobile): pin the reopened chat subscribe params without a cast

* fix(mobile): show a host-ended chat feed as an error instead of reopening it

With a token per subscription, the desktop ends a phone chat feed only when the
phone closes it or the socket drops: no client sends the connection-wide chat
unsubscribe, a socket close delivers no end, and every desktop with mobile chat
keys feeds by the client's token. The reopen-with-backoff layer and the
history-keeping reopen merge guarded a trigger that no longer exists, so they
are removed. An end that still reaches the screen settles the feed as an error,
so a dead feed is never shown as live; leaving and re-entering the chat
subscribes again with a fresh token.

* test(mobile): re-record the RPC goldens for the per-subscription chat token

Each nativeChat.subscribe now sends its own token, so the native chat scenarios
expect `claude:session-1:<id>` in the subscribe params, and the four native chat
recordings (native-chat-page-earlier and the session.native-chat-page matrix)
record that token in their subscribe and unsubscribe payloads. Every other
golden moved only its `baseline` header, repinned to the commit recorded from.

* test(mobile): repin RPC recordings to the merge with main and re-record

Merging main moved the product tree the recordings are pinned to, so the
baseline is repinned to the merge commit and the whole corpus re-recorded.
Only the four native chat recordings change content, because each
nativeChat.subscribe now carries a per-subscription token; every other
recording moves only its baseline header.

* test(mobile): repin RPC recordings to the merge with main and re-record

Merging main brought #22762's recordings, #23757's repin and #23720's recorder change, so the
corpus is repinned to the merge commit, the last commit to touch a recorded path, and re-recorded
whole. Against main, 786 recordings move only their baseline header and 4 native chat recordings
change content: their nativeChat.subscribe and nativeChat.unsubscribe carry the per-subscription
token instead of claude:session-1, which also moves their scenarioSha256.
2026-09-29 16:31:47 -07:00

33 lines
1.3 KiB
TypeScript

// Why: the runtime keys a native-chat transcript fs-watcher by the client's cleanup token
// (`agent:sessionId` when none is sent). A subscribing client must echo that exact token on
// `nativeChat.unsubscribe` so the watcher is closed when the chat view toggles off (not just on
// socket close) — otherwise watchers leak per session-switch. Centralizing the key shape keeps the
// token from drifting between the web runtime client and mobile.
export type NativeChatUnsubscribeRpc = {
method: 'nativeChat.unsubscribe'
params: { subscriptionId: string }
}
/** The cleanup token the server keys the transcript watcher under. An `instance` makes it one
* subscription's own, so two views of the same chat on one connection never evict each other. */
export function buildNativeChatSubscriptionId(
agent: string,
sessionId: string,
instance?: string
): string {
return instance ? `${agent}:${sessionId}:${instance}` : `${agent}:${sessionId}`
}
/** The unsubscribe RPC frame a client sends on teardown to close the watcher. */
export function buildNativeChatUnsubscribe(
agent: string,
sessionId: string,
subscriptionId?: string
): NativeChatUnsubscribeRpc {
return {
method: 'nativeChat.unsubscribe',
params: { subscriptionId: subscriptionId ?? buildNativeChatSubscriptionId(agent, sessionId) }
}
}