Skip to content

fix(desktop): know a Runtime Host target's identity before it connects - #5606

Merged
Astro-Han merged 1 commit into
mainfrom
fix/desktop-host-identity-before-connect
Sep 23, 2026
Merged

Astro-Han merged 1 commit into
mainfrom
fix/desktop-host-identity-before-connect

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Summary

On a cold start with a task open, the desktop shows "暂时读不到这个任务的权限" (boundary unreadable) and toasts for "刷新模型连接失败" and "载入本地记忆状态失败". They share one cause: every Host-scoped read issued before the Local Runtime Host finishes its first connection fails immediately instead of waiting.

Root cause

A target generation publishes its scope (epoch + hostId) while it is still connecting, but the desktop manager only learned the generation's hostId and created its reconnect lifecycle after the first connection succeeded. waitUntilReadyForScope therefore rejected scopes that main itself had published, and the awaitReady gate in preload turned every early scoped read into an error. The two routes that tell the renderer about scopes also disagreed: the runtime-host:identities pull and the runtime-host-profiles:changed push each re-derived hostId from different places.

The renderer's bounded boundary-read retry (#1629) and similar waits hid this window. #5494 made the window visible because it paints before the Host is ready.

Fix

The hostId is known when a generation is created: it is a remote profile's rootId or the Local storage root id. A reconnect lifecycle can also exist before its first connection.

  • createRuntimeHostReconnectLifecycle builds a lifecycle that exists before start(). waitForCurrent parks until the first candidate is installed and rejects if the start fails permanently. startRuntimeHostReconnectLifecycle is now create + start.
  • Every desktop target generation carries a required hostId and lifecycle from creation. This removes #startLifecycle, #requireLifecycle, the post-connect hostId assignment, and the optional-hostId spreads in boot, the profile service and the IPC pushes. The pull and the push now read the same field.
  • The renderer boundary read no longer retries on a timer, so readExecutionBoundaryWithRetry and its schedule are removed.
    • The IPC gate now waits for a Host that is starting or reconnecting.
    • The gate cannot cover a Host generation that is replaced or fails while a read is in flight, for example a profile rebind while the read is parked. For that case, the boundary is read again whenever the session's Host pushes ready, which is the event that actually makes the failure stale. The listener is armed for the whole active session, not only after a read fails, because the ready push can arrive before the read on the replaced generation fails.
    • useActiveExecutionBoundary moves into features/conversation. It now reaches sessions.readExecutionBoundary and Host changes through ConversationServices instead of window.maka. The renderer architecture ratchet does not allow the root-level hook or app-shell to take on a new bridge subscription.
  • runManagedLocalHostChange refuses while the Local target is still on its first connection, because suspending the lifecycle cannot hold back a connection that is already in flight.

Behavior change

  • Targets that are unavailable or never connected now publish their hostId in the push, which matches the pull. If the default Host fails to start, default-Host reads can surface their own error instead of being silently dropped. Targets that had connected at least once before already behaved this way.
  • Guest targets that are still connecting now get a scope, so their reads wait instead of failing.
  • stopSession and bot message delivery issued before the Local Host's first connection now wait for that connection instead of failing with "target has not started". Both call sites are fire-and-forget, and the wait rejects if the first connection fails permanently or the generation closes.
  • Because removal pushes now always carry hostId, preload no longer re-registers the scope of a removed target.
  • An unreadable boundary now recovers on its own once the session's Host reports ready. On main the timer retry recovered only when that happened within 1.75 s.
  • Not changed: awaitReady keeps its 15 s timeout. The preload active-Host wait and the isDefaultRuntimeHostResolvable guards stay because they also cover Hosts that are unavailable, not only ones that are starting.

Verification

  • runtime-host-desktop-manager.test.ts: 60/60 pass. Two new tests fail on main: a scope published before the first connection waits for that connection, and it rejects with that connection's failure.
  • active-execution-boundary-read-model: two new hook-level tests render useActiveExecutionBoundary behind ConversationServicesProvider and emit Host changes.
    • When the first read fails before ready, only a ready push for the session's own Host re-reads, and the boundary recovers. This fails when the re-read effect is disabled.
    • When ready arrives while the failing read is still in flight, the boundary still recovers. This fails when the listener is armed only after a failed read.
  • Affected suites pass: the manager, profile service, boundary read model, conversation services adapter, composer mentions, message queue and session reference composer suites together ran 120/120. runtime-host reconnecting-connection ran 25/25.
  • Desktop typecheck, typecheck:stories, npm run format, npm run lint, and check-renderer-architecture --strict-base against origin/main pass.
  • E2E: session-local-recovery and new-task-reload pass (5/5).
  • Cold-restart acceptance with a local scratch E2E (not committed): the test opens a task, restarts the app, and polls the page for 8 s.
    • On main, both toasts ("刷新模型连接失败", "载入本地记忆状态失败") appear.
    • On this branch, nothing appears in 2/2 runs and the composer stays visible.
    • It is not committed because the manager tests pin the root cause at the lowest layer that exposes it.
  • No screenshots: the healthy UI is unchanged, and only the cold-start failure states go away.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code (Opus 5.5) diagnosed the issue over CDP, wrote the fix and tests, and ran the verification above.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 23, 2026
@Astro-Han
Astro-Han force-pushed the fix/desktop-host-identity-before-connect branch 2 times, most recently from 4591757 to ec463f0 Compare September 23, 2026 03:01
@Astro-Han
Astro-Han marked this pull request as ready for review September 23, 2026 03:17
@Astro-Han
Astro-Han force-pushed the fix/desktop-host-identity-before-connect branch 4 times, most recently from 8cb9c69 to 587bcf0 Compare September 23, 2026 04:14
A target generation published its scope to the renderer while connecting,
but the manager only learned the target's hostId and created its reconnect
lifecycle after the first connection. waitUntilReadyForScope therefore
rejected every scope main itself had published, so each Host-scoped read
during a cold start failed fast instead of waiting.

The hostId is known when the generation is created (a remote profile's
rootId, or the Local storage root), and the lifecycle can exist before it
starts. Give every generation both from creation, and drop the places that
re-derived the hostId.

The renderer's boundary read retry schedule only papered over this window.
Replace it with the event that actually invalidates an unreadable boundary:
the session's Host reporting ready. The read model moves into the
conversation feature so it reaches the bridge through its services port.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the fix/desktop-host-identity-before-connect branch from 587bcf0 to 8611e4f Compare September 23, 2026 04:46
@orangeCatDeveloper

Copy link
Copy Markdown
Contributor

LGTM

@Astro-Han
Astro-Han merged commit d5bc0fa into main Sep 23, 2026
1 check passed
@Astro-Han
Astro-Han deleted the fix/desktop-host-identity-before-connect branch September 23, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants