Skip to content

fix(mobile): let the host's transcript and catalog be the single mobile authority - #3201

Merged
wgqqqqq merged 7 commits into
GCWing:mainfrom
wgqqqqq:wgq/mobile-transcript-authority
Sep 23, 2026
Merged

wgqqqqq merged 7 commits into
GCWing:mainfrom
wgqqqqq:wgq/mobile-transcript-authority

Conversation

@wgqqqqq

@wgqqqqq wgqqqqq commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

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.

  • Rust relay/core: a relay read resolves one requested turn through the derived turn catalog (with a deliberate full-scan fallback) and logs which mode served it; a persisted InProgress turn is no longer reported as running merely because this process does not own it.
  • Shared Kotlin core: sessionsRevision / workspacesRevision hints 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.
  • HarmonyOS: draft text is owned by the submission that carried it, session and workspace rows resolve through keyed identity plus a current-snapshot lookup, streaming markdown treats host rewrites and deletions as authoritative, and an on-device catalog refresh fixture plus tool tests were added.
  • iOS: a remote send settles against the draft revision it cleared, streaming text state treats host rewrites and deletions as authoritative, and the transcript path gained DEBUG-only timing.
  • Android: a submitted draft survives activity recreation, the composer stays editable while a session hydrates, and the compact drawer paints its scrim with the semantic scrim token.

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.

  • A late acknowledgement for an older send could clear a draft the user had already retyped.
  • History paging paid one full transcript materialization per page, and an older page could be served without the log it had to answer from.
  • A turn left InProgress on 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 Cancelled with finish_reason = interrupted in 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 is workspace-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 commit 9f874c2c3; 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_binding fails identically on a clean checkout of upstream/main (d06151243, same assertion at coordinator.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 commonTest suites (Gradle/Android SDK), the iOS XCTest and Android instrumentation targets (Xcode / device), and tools/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:

  • When a host omits 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.
  • The composer gates are client-side affordances only; the relay still rejects a command from a disconnected surface, and stopping a turn requires a connected host rather than a reachable one that is mid-reconnect.
  • The relay read remains a read: the abandoned-turn projection changes the response, never the persisted turn.

Known gaps that exist in the base and are deliberately not changed here:

  • HostStreamState::evict clears record_seq but not record_index, while diff_records re-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.
  • The HarmonyOS stream client writes truncated (HostSessionStream.ets:228 and :278) but never reads it, so a possibly short history page is not surfaced as a degraded state.
  • tools/tests/composer-submit.test.cjs supplies a stub committer for the send path, so the real ConversationCoreState.prepareComposerSubmission interaction (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

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

Generated with OpenBitFun

wgqqqqq and others added 6 commits September 21, 2026 19:59
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
wgqqqqq marked this pull request as ready for review September 22, 2026 12:05
@wgqqqqq
wgqqqqq merged commit d6f9410 into GCWing:main Sep 23, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant