fix(mobile): let the host's transcript and catalog be the single mobile authority - #3201
Merged
Merged
Conversation
Reopening a session published the transcript stored on this device before the host answered for it. A stored copy stops wherever its last write stopped — inside the turn that was running when the app went away, because a streaming assistant message is not durable yet — so the pane showed a half-finished turn and every gate read "the transcript arrived": the skeleton ended, the scrolling settled, and the composer offered itself as if the session were all there. The rows are still shown at once; what changes is that the state now says who they belong to. - core-domain: `ChatTranscriptOrigin` on `ChatTimelineState`, CACHE for rows restored from this device's copy and HOST once the host replays or streams them. `reset()` goes back to CACHE with the empty state. - core-feature: `open()` marks CACHE where it publishes the stored copy; the stream, the caught-up callback, and a `relay://session-gap` replay mark HOST. `persistTranscript` writes back only a HOST view, so a restored copy can never be stored as though the host had confirmed it. - core-feature: `transcriptUnconfirmed()` is the app-visible spelling of that fact, so a platform app can act on it without depending on :core-domain. - iOS and Android: the open wait ends on the host's rows rather than on any rows, the skeleton stays hidden when the pane already has the stored copy, and that copy carries a "syncing" row until the host's transcript lands. HarmonyOS is unchanged: it never publishes this device's stored copy on open (its durable stream is the only source), so it has no restored-transcript state to label.
Relay reads now resolve a single requested turn through the derived turn catalog and fall back to the full transcript scan when the catalog cannot answer, logging which mode served the read. A turn whose persisted status is InProgress is no longer reported as running merely because this process does not own it: a loaded owner supplies runtime state, and for an unloaded session an exclusive writer lease proves no other process is executing that session. Only then does the observer projection mark abandoned turns Cancelled with finish_reason interrupted, and the persisted records keep their original status for recovery. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
…ost to send HostCatalogObserver consumes the host's sessionsRevision and workspacesRevision hints with a coalescing floor instead of invalidating on every event, so a burst of host changes no longer costs one refresh each. RemoteSessionStore and RemoteWorkspaceStore keep session and workspace invalidation on separate paths and suspend refreshes while a turn is in flight. Composer send and the new stop action require a connected host instead of treating a reconnecting transport as reachable, because navigation may keep an offline session on screen and that is not authority to command its last-known execution state. Relay account requests log their timing. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
… host authority Draft text now belongs to the submission that carried it: ComposerSubmission commits or rolls back against a revision, so a failed send keeps the text the user typed instead of clearing it or consuming a newer draft. Session and workspace rows resolve through keyed identity and a current-snapshot lookup instead of a stale local index, the sidebars, the picker, and the remote home read the same host catalog, and streaming markdown treats a rewritten or deleted block from the host as authoritative rather than discarding it as an out-of-order chunk. Adds the on-device catalog refresh fixture with tools/check-catalog-refresh.py and the composer-submit and streaming-markdown tool tests, and documents those entry points in the harmonyos guide. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
A submitted draft now survives activity recreation through rememberSaveable, so restoring the process cannot resurrect text the user already sent. The draft field stays editable while a newly opened session hydrates, while send and attachment actions remain guarded by busy. Stopping requires a connected host, session rows stay tappable so a slow transcript can be superseded, and the compact drawer paints its scrim with the semantic scrim token rather than the page background, which made opening the sidebar look like the detail page had disappeared. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
A remote send now records the draft revision it cleared, so an acknowledgement that arrives after the user typed again no longer wipes the newer text, and a send only settles while that revision is unchanged. Streaming text state treats host rewrites and deletions as authoritative instead of discarding them as stale chunks, stopping a turn requires a connected remote host, and the transcript path gains DEBUG-only timing so a slow open or apply is attributable. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
wgqqqqq
marked this pull request as ready for review
September 22, 2026 12:05
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The mobile surfaces now treat the host as the single authority for transcript, catalog, and execution state, instead of rebuilding that state from a local copy.
InProgressturn is no longer reported as running merely because this process does not own it.sessionsRevision/workspacesRevisionhints are consumed with a coalescing floor, session and workspace invalidation stay on separate paths, refreshes are suspended while a turn is in flight, and composer send/stop require a connected host.Fixes: no linked issue.
Type and Areas
Type: bug fix / regression fix
Areas: Rust core (
assembly/core,services-integrations), mobile iOS, mobile Android, mobile HarmonyOS, mobile shared Kotlin core, mobile tooling/tests.Motivation / Impact
Three user-visible failures shared one root cause: a mobile surface could act on a locally rebuilt view of host state instead of the host's own record.
InProgresson disk by a stopped executor was reported to the relay as still running, so a reconnecting client could show a live turn that nothing was executing.An abandoned turn is now projected as
Cancelledwithfinish_reason = interruptedin the relay read only; the persisted record keeps its original status so recovery and on-disk evidence are unchanged.Verification
cd src/apps/mobile/harmonyos && node --test tools/tests/*.test.cjs— 193 tests, 192 passed, 1 failed. The failure isworkspace-editor.test.cjs"sidebar final entry hierarchy keeps modes in workspace plus and tools in the footer", which also fails on a clean worktree at this branch's pre-change commit9f874c2c3; that test and its subject file are not touched by this branch.cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib agentic::coordination::coordinator::tests::— 111 passed, 1 failed.remote_session_metadata_never_falls_back_to_a_local_workspace_bindingfails identically on a clean checkout ofupstream/main(d06151243, same assertion atcoordinator.rs:15164), so it is inherited from the base, not introduced here, and that test is not part of this diff.cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib agentic::persistence::manager::tests::— 56 passed, 0 failed, 1 ignored.pnpm run check:repo-hygiene— passed (product identity audit, filename and content rules).pnpm run fmt:rs— applied to the changed Rust files.Not run in this environment: the Kotlin
commonTestsuites (Gradle/Android SDK), the iOS XCTest and Android instrumentation targets (Xcode / device), andtools/check-catalog-refresh.py(requires a real HarmonyOS device over hdc). Those changes are therefore covered only by review, not by an executed test here.Reviewer Notes
Compatibility and remote scenarios:
sessionsRevision/workspacesRevision, the Kotlin and ArkTS observers keep the conservative invalidate-everything path, so an older host degrades to today's behaviour rather than to a missed invalidation.Known gaps that exist in the base and are deliberately not changed here:
HostStreamState::evictclearsrecord_seqbut notrecord_index, whilediff_recordsre-sends only when a record's hash changed, so an evicted session-record cannot be re-materialized by a later full synchronization. Fixing it re-inserts records at their original sequence rather than the tail, which is a separate change with its own ordering risk.truncated(HostSessionStream.ets:228and:278) but never reads it, so a possibly short history page is not surfaced as a degraded state.tools/tests/composer-submit.test.cjssupplies a stub committer for the send path, so the realConversationCoreState.prepareComposerSubmissioninteraction (revision comparison, single rollback, draft survival after a failed send) is exercised only on the steer path.The third checklist item below is intentionally left unchecked: this change set was not audited for new user-facing strings or locale keys.
Checklist
Generated with OpenBitFun