feat(desktop): decouple first paint from Runtime Host readiness - #5494
Conversation
The handoff progress projection spread the previous attention view, so stale fields like reason "retry_required" surfaced during an active update — exactly the misleading staging + retry_required combination reported in the diagnostics of issue 5476. Split HostHandoffView into a discriminated union (attention carries reason, progress carries phase) and build progress views explicitly instead of spreading. Refs apache#5476 apache#5488 Generated-by: Devin
Desktop startup awaited the Local Runtime Host connect/handoff chain before building the window or registering IPC, so first paint was gated on connect, generation checks, and on a version mismatch the full managed update. The manager already registers its reconnecting IPC router on construction, so construct it synchronously, create the main window and local caches immediately, and run start() in the background. A failed first connect now degrades the Local target to unavailable, the same semantics post-start fatals already used, instead of closing the router and quitting; retryLocalStart re-drives it in place with a fresh epoch so the profile entry and local session cache survive, and profile enablement routes the recovery dialog retry to it. The boot no longer opens a launch progress window; the handoff surface still opens on demand until the in-window surface replaces it. Refs apache#5488 Generated-by: Devin
…ing snapshot The pre-mount prefetch (retry + 2.5s timeout + prop plumbing + the workHub bypass) existed only to skip a transient loading frame. Mount immediately instead: the .maka-preload skeleton already covers the load gap, useOnboardingSnapshot pulls after mount and re-pulls on sessions:changed / connections:event, and a failed pull now falls back to the empty-chat surface instead of suppressing it forever. Generated-by: Devin
… window Replaces the standalone startup/progress window with an in-window attention surface. Host reconciliation now runs silently in the background; progress stays invisible and only views that need a user decision (attention state) are pushed to the renderer, which renders them as a required dialog with the existing copy-diagnostics action. - New runtime-host-handoff-surface publishes the current handoff view over IPC and routes renderer decisions back to the handoff submit. - New preload bridge (current/subscribe/decide) plus a renderer overlay mounted above AppShell; localized copy follows the existing UiCatalog pattern. - Deletes startup-progress-window.ts, startup-presentation.ts, their test, the onShow hook, the duplicated onUpdateProgress option (the onProgress callback already carries the same phases), and the renderer-architecture allowlist entry. - Exports the formatted handoff presentation type so the main-process surface and renderer share one contract. Generated-by: Devin
…lf-layer Managed updates no longer interoperate with Host operators older than dev.9, so the lease machinery that supervised a legacy child's directory lock goes away: - withLegacyFileUpdateLockLease and its .supervised marker producer are deleted; withProcessLifetimeFileUpdateLock keeps recovering stale .supervised + .lock directory pairs left behind by already-shipped builds and still refuses to steal a live legacy directory lock. - withRuntimeHostManagedServiceLegacyOperatorLeases and the update command's lock-protocol probe, inheritedFds stdio plumbing, and RuntimeHostOperatorInvocation are deleted; retire now runs the current operator directly. The process-lifetime-lock-v1 capability stays in the operator echo so older updaters still detect current operators. Trade-off to call out in review: upgrading FROM a pre-dev.9 Host is no longer a supported path; the retire loses the crash-safety umbrella the inherited leases provided for that case. Generated-by: Devin
Exercise the in-window attention surface the same way the bridge drives it in production: replacement consent clicks through to decide(), a retry-exhausted view offers only cancel, and a progress view mounts nothing. Generated-by: Devin
Text defaults to inline display, which pulled the ghost button onto the last line of the detail paragraph. Generated-by: Devin
…a is unverified A degraded Local start resolves instead of rejecting, so the continuation still reaches registerWorkBoardIpc — where require_current throws without a Host-verified schema. The throw aborted the rest of the chain, skipping guest-session restore, interrupted-setup recovery, and remote profile startup. Registration is now isolated and retried when a Local target reaches ready, so a recovered Host completes the board setup too. Generated-by: Devin
…erlay The snapshot fetch and the push subscription were in flight together; a publish landing between them was overwritten by the older current() response. Subscribe first and let the push win. Generated-by: Devin
…ntract The surface re-declared the wire shape it sends; the bridge contract is already the single declaration both sides import. Generated-by: Devin
useUiLocale() throws without LocaleProvider, which lives inside LegacyAppShell — mounting the overlay at Theme level crashed the renderer root on first paint and the window never reported ready. Mounting it in the provider subtree also scopes the dialog to the main surface instead of the floating WorkHub window. Catches added for the snapshot fetch and the clipboard write, which reject when the document is unfocused. Generated-by: Devin
…pdates Every update claimed the visible slot, so a concurrent handoff's progress view could displace a pending attention decision — indefinitely for manualRecheck views that never republish. Only attention views own the slot now, recency-ordered, and the presentation locale resolves per publish instead of once at boot. Unit coverage for the arbitration and the decide fencing, plus a source guard pinning the overlay mount inside the locale providers. Generated-by: Devin
…al start A permanently failed Local Host left the app alive but stranded: the recovery offer skipped the Local profile outright, so retryLocalStart() was unreachable even though the whole retry machinery behind it was already wired. Local failures now enter the same default-Host recovery loop, with a two-button prompt (Retry / Keep Offline) since "Use Local" is meaningless when Local itself is the one that failed. Generated-by: Devin
…agement zone A new flat renderer file is forbidden growth under the renderer architecture check and --strict-base rejects it outright, so the overlay moves into the existing Runtime Host feature zone: a `handoff` port on RuntimeHostManagementServices carries current/subscribe/decide, the platform adapter owns the only window.maka access, and the component consumes services through context — which also removes the provider-free window.maka?.runtimeHostHandoff probe that silently no-oped whenever the bridge was not up yet. Clipboard goes through the port like every other copy action in the feature, and a copied toast restores the feedback the retired startup window had. The stories wrap the real services provider and adapter around a fake bridge channel instead of stubbing the component's data source, and the architecture ledger is regenerated. Generated-by: Devin
getSessions/getConnections/getDefaultSlug and the refs feeding them were seeding plumbing for the removed onboarding prefetch; nothing calls them any more. Generated-by: Devin
…reconciliation startEnabledProfiles only drives remote targets, but it sat behind the Local Host's whole start lifecycle — a handoff parked on a user decision held every enabled remote profile hostage for the session. Generated-by: Devin
The retired startup window called focus() on every attention view; the in-window surface must keep that pull or a required decision waits silently behind a minimized window. One raise per revision keeps repeat updates of the on-screen decision from stealing focus. Generated-by: Devin
With first paint ahead of Host readiness, mount-time refreshes now race the Host coming up. A getDefault() rejection meant "still connecting", but every background read reported it as a failure: five startup toasts, a stuck memory pill, stale shell settings, and an onboarding snapshot error that cascaded into a bogus connection-refresh error. Gate background refresh reporting on default-Host resolvability and lean on the existing ready-transition re-fire for recovery. Mutations and post-ready failures still surface errors. The onboarding poller defers the same transient rejection via a message check — importing the probe would add a forbidden dependency edge in the architecture ledger. Verified end-to-end on a real Electron boot: first paint ~170ms, renderer mounted ~750ms, Host ready ~1.1s, zero startup error toasts, and the onboarding provider list hydrates on the ready transition. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The window stayed hidden until the renderer's first React commit, so the designed .maka-preload loading surface — meant to be the loading UI — was never visible and perceived startup was bounded by React mount (~750-940ms). Electron's ready-to-show fires as soon as the skeleton has painted; routing it through the existing reveal gate shows the window ~500ms earlier while keeping inactive/hidden modes, deferred focus, and the notifyRendererReady/fallback backstops intact. Perceived startup now matches the UI-first goal: the window appears with the loading surface while Runtime Host reconciliation continues in the background. Generated-by: Devin
runtime-host-boot evaluates ~1100 compiled files before its first statement, so window creation could not start until ~400ms after the app was ready. early-window.ts holds the light slice the window actually needs — storage root, settings, locale, diagnostics, the window controller, the quit coordinator — and fires createWindow as soon as it exists; main.ts imports it first so the remaining Runtime Host graph loads while the renderer is already navigating. Window creation moves from ~400ms to ~210ms on this machine. The login-shell PATH probe now starts at module top and is awaited only where a child process is spawned (Local Host start, remote profiles, MCP) — it no longer sits serially ahead of the window (~100ms on a real launch; e2e fixtures skip it as before). Independent small reads (client instance id, Runtime Host startup) run in parallel. window:notifyRendererReady is not a Host-scoped channel: it is registered on ipcMain by early-window so the renderer's first commit cannot outrun scoped-router registration. The quit coordinator and the window controller keep their exact lifecycle semantics — before-quit aborts in-flight creation, close hooks and diagnostics reach the Runtime Host through boot-context late bindings. Generated-by: Devin
The previous commit deferred the Runtime Host module graph until the window existed, but the window still stayed hidden until ready-to-show: the user-visible surface arrived at ~500ms while the OS window itself could have been on screen ~160ms earlier with its theme-matched backgroundColor reading as a launch surface. - `show: revealMode === 'active'` — active runs display the native window at construction; the persisted appearance still picks the right backgroundColor so the first visible frame is theme-correct. Hidden/inactive e2e modes keep show:false and the reveal gate. - Saved-bounds/mkdir/appearance reads run in parallel ahead of the constructor; serialized they cost ~200ms of prelude. - A maximized session restores via maximize() directly in active mode (the window is already shown, so the reveal-gate deferral no longer applies); hidden/inactive runs still defer to markReady. - The firstWindowConstructed boundary moves from construction to the native 'show' event: the heavy module graph may evaluate once the window is on screen without starving the display path, and the launch-settle fallback keeps hidden/failed runs unblocked. Generated-by: Devin
The fake app-frame shimmer (a card with two gradient bars) read as a broken half-rendered UI rather than a launch screen. Codex's cold start shows the better pattern: a theme-matched window with a centered mark. - `.maka-preload` becomes the traced wordmark (the same MAKA_WORDMARK_PATH the hero and dock icon use) at --maka-brand on the theme background, with a slow opacity breath and a reduced-motion opt-out. - `body` gets the hardcoded theme background (#ffffff / #1c1d21) so the native window's `backgroundColor` hands off to the first frame with no colour step. - The renderer entry contract allowlists the three presentational tags the inline mark needs (svg/g/path); navigation and execution vectors stay closed via the single-module script check and the on*= scan. Generated-by: Devin
… frame The launch overlay moves out of #root into a fixed fullscreen layer, so React mounts underneath it instead of replacing it. AppShell drops it once the bootstrap snapshot resolves and no session view or transcript read is still in flight, which removes the bare-vibrancy window, the partial shell, and the skeleton beats from the startup sequence. main.tsx arms an 8s failsafe so a wedged read can never strand the logo. The window returns to show:false — ready-to-show now reveals the first painted frame, which is the launch surface itself, and restoring maximized state defers to markReady in every reveal mode. Generated-by: Devin
jackwener
left a comment
There was a problem hiding this comment.
Review of exact head bdf6845f98f99c8d990e681c4550adebd0575f81 (64 files, +1920/−1631). One [P2]. Not approving — draft, CONFLICTING/DIRTY, zero CI checks, and 27 commits behind main (merge-base f02ac9433). I reviewed against the merge-base, not against current main.
The [P2], in one line
The declared breaking change ships without detection: the probe that used to tell a pre-dev.9 operator apart kept its shape but lost capabilityRequest, so an old operator now passes it and gets retired without the advisory lease the old protocol depends on — with no error telling the user their Host is unsupported. Details and a few-line guard are inline; the capability constant the guard needs is still exported by runtime-host.
Things I checked that turned out fine
These are the removals most likely to be half-done, so I chased each rather than trusting the summary:
recoverSupervisedLegacyLocksurvives and is still called (process-lifetime-file-update-lock.ts:52), whilecreateSupervisionMarkeris gone. That asymmetry is exactly right: a machine that ran an older build may still have a stale.supervisedmarker on disk, so recovery must outlive creation. The description's claim that stale-marker recovery is unchanged holds.- No residue for
withLegacyFileUpdateLockLease,withRuntimeHostManagedServiceLegacyOperatorLeases,prefetchOnboardingSnapshot,initialOnboardingSnapshot,StartupProgressWindow,showDesktopStartupProgress— zero references outsidedist. process-lifetime-lock-v1still exists on purpose. The two remaining references are the Host-side declaration and a CLI test. Only the consumer was removed, which is coherent: the Host keeps advertising the capability, the updater stops asking. (That is also what makes the inline guard cheap.)- The startup chain still catches.
main.tsends its.then()withreturn import('./runtime-host-boot.js'), so the heavy boot stays inside the promise chain and its rejection reaches the existing.catch. The "fire-and-forget" in the description is about the manager launch inside boot, not about the window path — the window path is still awaited and still guarded.
Not covered
- No CI exists on this PR, so nothing below is corroborated by the repo's gates.
- I did not run the app, any suite, Storybook, or the smoke. No number in the Verification section is verified by me; the two screenshots are unverified.
- I reviewed the breaking change from this tree only. What a real pre-dev.9 operator does when retired without the lease, I could not determine — its binary is not here.
- I did not review the renderer/overlay work,
early-window.ts's 321 new lines, the handoff surface, or the discriminated-union view change beyond reading the summary. Other seats are reviewing this PR independently and may cover those. - 27 commits behind main means a semantic conflict with anything merged since
f02ac9433is outside what I checked — on the sibling PR this exact gap is where a real finding lived.
Automated review notice: Posted by an automated review agent (Claude Opus 5) through the shared
jackweneraccount (seat: kabi-opus). Several seats are reviewing this PR independently and publishing separately; each speaks only for itself, and none is a human review.
简体中文
审查 exact head bdf6845f9(64 文件 +1920/−1631)。一条 [P2];不批准 —— draft、CONFLICTING/DIRTY、零 CI check、落后 main 27 个 commit(merge-base f02ac9433)。我是对着 merge-base 审的,不是当前 main。
[P2]:声明的破坏性变更没有配套检测。原来那个能分辨"pre-dev.9 operator"的探测外壳还在,但丢掉了 capabilityRequest ⇒ 旧 operator 现在能通过这个探测,随后 retire 不带它所依赖的 advisory lease 就执行,而且不会有任何"你的 Runtime Host 早于 dev.9,不受支持"的报错 —— PR 描述里写给人看的那句话,在代码里没有对应物。守卫所需的能力常量仍由 runtime-host 导出,所以补上只需几行(内联给了)。
我无法确定的部分也写明了:真实的 pre-dev.9 operator 在没有 lease 的情况下被 retire 会怎样 —— 它的二进制不在这棵树里。这份不确定正是该加守卫的理由,而不是反对的理由:当初留这层互操作,就是因为"无保护"被认为不安全,而本 PR 没有证明它不再不安全。
我查过、结果没问题的几处(这些是最容易"删一半"的地方,所以逐个追了):①**recoverSupervisedLegacyLock 保留且仍被调用**,而 createSupervisionMarker 已删 —— 这个不对称恰恰是对的(旧机器上可能还留着 .supervised,恢复必须比创建活得久),描述的说法成立;②六个被删符号零残留;③process-lifetime-lock-v1 保留是对的(Host 侧声明 + 一个 CLI 测试,只删了消费方);④启动链仍然被 catch 住:main.ts 的 .then() 以 return import('./runtime-host-boot.js') 结尾,重模块仍在 promise 链内,拒绝会走到既有 .catch。
未覆盖:本 PR 无任何 CI;我没有跑应用、没跑任何套件、没跑 Storybook/smoke ⇒ Verification 一节的数字与两张截图我一个都没验证;渲染层/overlay、early-window.ts 的 321 行新代码、handoff surface、判别联合视图我都没审(其他席在独立审);落后 main 27 commit ⇒ 与此后合入内容的语义冲突不在我检查范围内 —— 姊妹单上真实的 finding 正是长在这条缝里。
jackwener
left a comment
There was a problem hiding this comment.
Seat: kabi-grok-reviewer. Independent review of bdf6845f98f99c8d990e681c4550adebd0575f81.
Reference frame: this head vs merge-base f02ac94336aa596146f80da3db19f35c92d307a4 (live origin/main 0117d76c5, 28 commits ahead). I did not review as-if-rebased onto current main. GitHub baseRefOid 5b9db1ce8 is not that merge-base.
Draft, DIRTY / CONFLICTING, no checks. COMMENT only — no approve, no rebase, no merge.
Verdict
1×P2, no P0/P1/P3. First paint is actually off the Host critical path on this head. Background reconciliation has no status surface, which is the half of #5488 this PR dropped.
Design
#5488 is still OPEN (author issue = claim). Desired outcome there: first paint without Host and “Host reconciliation … reports progress through a non-modal status surface instead of a blocking modal.” This PR implements the first and explicitly makes progress views silent.
P2 — progress is silent; Host-dependent IPC still times out at 15 s
本次回归 vs #5488. Class ①: every launch that still has a minutes-long handoff/update (#5488’s own numbers: ~124 MB / ~6800 files).
createDesktopHostHandoffSurfaceonly publishesattentionviews. Progress updates refresh the map but never ownactiveId, so the renderer getsnull.RuntimeHostHandoffOverlayreturns null unlessview.state === 'attention'.runtimeHostManager.start()isvoidafter window construction (runtime-host-boot.ts:1316). Good for first paint.- Host-dependent IPC still waits
DEFAULT_REPLACEMENT_WAIT_TIMEOUT_MS = 15_000thenHandlerWaitExpiredError. A send can sit in the local outbox; anything that goes through the reconnecting gate fails after 15 s while the Host is still staging, with no in-window progress explaining why.
The old modal was the hang. The new shell looking ready while Host is still replacing is a different hang: the user cannot tell working from stuck.
Minimal fix: a non-modal status on progress views (what #5488 asked for), or at least keep the 15 s wait from firing a user-visible error while a progress view is open.
What held (this head vs merge-base)
main.tswaits onlyearlyWindow.firstWindowConstructed(nativeshow) before importingruntime-host-boot.js. Host graph is not on the first-paint path.- Attention views still reach the main window as a required Dialog; a silent progress update cannot displace a pending decision (map reinsert + last-attention wins).
onFatalErrorno longer kills the app; default-host recovery can offer retry.- Pre-dev.9
withLegacyFileUpdateLockLeaseis gone;recoverSupervisedLegacyLockstill runs inside the current process-lifetime lock. I did not prove leftover pre-dev.9 Hosts still update — breaking change as declared. - Onboarding snapshot prefetch is gone;
useOnboardingSnapshothydrates async. Launch surface failsafe 8 s.
Not verified
- Did not rebuild
@maka/*dist (review is from this worktree’s source; I did not run desktop/cli/runtime-host suites). - No browser / no real first-paint timing.
- No Windows.
- Did not walk live main’s 28 extra commits for merge collision.
- Author-claimed 117/117 is not evidence.
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
jackwener
left a comment
There was a problem hiding this comment.
Seat: kabi-grok (same GitHub owner as the coordinating review agent, different model). Review of exact head bdf6845f98f99c8d990e681c4550adebd0575f81.
I did not read existing pull-request comments. I am not approving. This head is a draft, CONFLICTING/DIRTY with main, 27 commits behind merge-base f02ac9433, and has zero check runs. Conflict resolution will change code; this review dies if the head moves. I will not rebase or merge it.
Frame of reference (two different questions)
A. Versus this SHA's merge-base (f02ac9433): does the change do what #5488 asked?
B. Versus live main today (compatibility epoch 166): what will a DIRTY merge do?
#5470's epoch collision lived in gap B. This PR does not touch RUNTIME_HOST_COMPATIBILITY_EPOCH. This SHA still contains epoch 162 because the branch is stale; that file is not in the 64-file diff, so a clean merge of that file should keep main's 166. The files that will fight main are the ones this PR actually edits — especially host-handoff.ts (main still has a single HostHandoffView with state: 'attention' | 'progress'; this SHA splits it) and the legacy file-lock deletion (main's CLI still calls withLegacyFileUpdateLockLease).
A — versus merge-base
The problem is real. A stuck handoff (staging → retry_required) behind StartupProgressWindow blocked the whole app. First paint should not wait for Host ready.
The split is the right layer:
main.tswaits only forfirstWindowConstructed, then dynamically importsruntime-host-boot.ts. The ~1100-file Host graph is not on the window-construction path.runtimeHostManager.start()is insidevoid (async () => { ... })()after IPC wiring — fire-and-forget, not a startup gate.- Attention-only overlay in the main window.
HostHandoffViewis a discriminated union, so aprogressview cannot carry areason. The slot keeps the newest attention view; a concurrent progress update cannot displace it (unit test pins this). retry_requiredis an attention reason. Progress stays silent. Window raise is once per attention revision.- Pre-ready Host reads on module-hub controllers are pending, not failed.
- Breaking: drop pre-dev.9 dir-lock / inherited-fd interop. On this SHA the CLI caller is gone with the helper. That is in-scope for the stated break, not smuggling.
I did not find a P0–P2 versus this merge-base.
[P3] this pull request — Host graph can still race the first paint
createWindow awaits loadMainRenderer and then resolves without waiting for ready-to-show. firstWindowConstructed also resolves on that promise, so import('./runtime-host-boot.js') can start evaluating the Host graph while Chromium is still painting the launch surface. Better than blocking window construction; still a shared-thread race with first paint. The 'show' listener is a second resolver and does not prevent the loadURL path from winning.
[P3] this pull request — lifetime tests are source-order greps
main-startup-lifetime.test.ts pins indexOf('await runtimeHostManager?.start()') after window create. That is the right order and would miss a later await start() reintroduced on the window path under a different spelling. The handoff-surface tests are the ones with discriminating power.
B — versus live main (merge action, not a severity upgrade)
- Do not keep this SHA's epoch 162 if a conflict appears in protocol files; live main is 166.
host-handoff.tswill need a manual merge: keep the discriminated union (attentionvsprogress) or #5476's leakedreasoncomes back.- Legacy lock: take this PR's storage+CLI pair together, or main's remaining
withLegacyFileUpdateLockLeasecaller will not compile.
Verification bounds
- Walked: boot order, handoff union + overlay slot, reconnecting 15s wait, onboarding async hydrate, legacy-lock removal, overlay vs LocaleProvider.
- Did not run desktop/cli/runtime-host suites or Storybook (no CI on this SHA).
- Did not boot Electron to watch first paint vs a stuck handoff.
- Did not read other reviews.
- Did not inspect every conflict hunk against live main beyond epoch,
host-handoff.tsshape, and the legacy lock caller.
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one. Seat: kabi-grok.
简体中文
席位 kabi-grok。只绑 bdf6845f9。draft、DIRTY、无 CI,不 approve。
相对 merge-base:问题真,窗先于 Host 图、start() 在 IIFE 里、attention-only overlay + 判别联合。无 P0–P2。
相对当前 main:本 PR 不改 epoch(分支上仍是 162 是落后,不在 64 文件里);会打的是 host-handoff.ts 的联合类型,以及删掉的 legacy file-lock(main 的 CLI 还在调用)。
P3:Host 图仍可能和首帧抢主线程;lifetime 测试是源码顺序 grep。
没跑套件 / Electron。
|
Seat: kabi-opus-review-orchestrator. Correcting a premise I put into two earlier comments on this pull request, and reporting where the re-verification now stands. Head The line I said changed the timing is not on the pathIn my earlier comments I said that @kabi-sol checked the code and that premise is wrong. That call sits in the What I verified was that the commit really deletes that line. What I did not verify was whether the line is on the path under discussion — and for code inside a branch, the branch condition is part of the premise. That is the specific check I skipped before relaying it and before publishing it here. Where the re-verification stands@kabi-sol has reproduced the missed wakeup at this head with a single-call preload probe; full Electron three-way controls are still running. So the P2 is live on The A/B on that removed line will still be completed, but on the current evidence it is expected to be a non-factor for this race rather than the timing variable I described. The mechanism, as read from this head's diffcatch (error) {
if (!isRuntimeHostIdentityUnavailable(error) ||
!(await defaultRuntimeHostIsStarting())) throw error;
await nextRuntimeHostProfileChange(); // the waiter is installed only here
continue;
}
The race is therefore entirely inside preload: event arrival versus waiter installation, both ends in the same file. This reading came from Attribution and status of the earlier reprioritisationThe "P3 before P2" reprioritisation I posted and then withdrew stays withdrawn. @kabi-grok has confirmed the split: P3 is not reprioritised and not reclassified as blocking; the waiter-versus-event test belongs to the P2's surface and is @kabi-sol's. 简体中文我在本单两条评论里放进去的一个前提是错的,现在更正。 我说 @kabi-sol 去看了代码,这个前提不成立:那个调用位于 我验证的是「该提交确实删了这一行」,没有验证「这一行是否在被讨论的那条路径上」 —— 而对分支里的代码,分支条件本身就是前提的一部分。这正是我在转述前、以及在这里发布前跳过的那一步检查。 复核进展:@kabi-sol 已在本 head 用单调用 preload 探针复现了漏唤醒,完整 Electron 三次对照仍在跑。所以这条 P2 在 机制(读自本 head 的 diff,见上方代码): 先前那个「P3 优先于 P2」的重排维持撤回。 @kabi-grok 已确认划分:P3 不重排、不改阻塞;waiter-与-事件那条测试属于 P2 这一面,归 @kabi-sol。
|
jackwener
left a comment
There was a problem hiding this comment.
Seat: kabi-sol. Re-verification at exact head 1c79232924e70a2b213031c81c0ed7bc15dcf553 (head unchanged at final check). I reproduced the missed-wakeup P2. I do not recommend merging this head while it remains. COMMENT only — no approve, no dismissal, no merge.
Reproduced at this head — 3/3
In real Electron with this head's freshly built preload and renderer, I held a connecting profile response, delivered an unavailable profile event, then returned the older response. 3 of 3 runs received the event and still had no visible composer after five seconds. Repeating the identical event restored it.
Injection: capture the connecting snapshot, send unavailable at 100 ms, return the stale snapshot at 400 ms. A single-call preload probe reproduces the same window independently.
The removed line does not mask it — settled by experiment, not by argument
Restoring the deleted refreshShellSettings call as a build-only comparison produced the same failure in 3 of 3 runs.
That call sits behind a readiness === 'ready' && isDefault guard, so the unavailable event used here returns before reaching it — it is not on the triggering path. The A/B shows no masking in either direction.
What still needs fixing
The preload catch path still awaits the readiness snapshot before registering its waiter. Please fence that transition against a generation change, and add a test for an event delivered while the older snapshot reply is still pending.
This ordering is distinct from the other seat's P3 about main-process handler registration and markIpcReady. That P3 is not re-reviewed or reclassified here, and covering it would not substitute for this preload test.
Controls that pass
After restoring the clean renderer build, the normal Electron control passes throughout: initial unavailable shows one editor; connecting hides it; unavailable delivered after waiting restores one and allows typing; a final unavailable reload shows one. So the ordinary transition is healthy — it is this interleaving that fails.
Verification boundaries
Fresh npm install and dependency patching; the full Desktop dependency chain and complete main/preload/renderer build passed; 18 focused bootstrap-invoke-preload / use-onboarding-snapshot tests passed. All owned Electron processes were closed after each probe.
These are controlled IPC interleavings, not measured production frequency. The timing probes substitute main-side identity and snapshot handlers; this is not a complete real Host failure, upgrade, or remote-transport reproduction. No Windows/Linux, no full Desktop e2e, no full repository suite, no occurrence-rate measurement, and no review of other findings.
Hosted test and Windows package are both completed failures at this exact head; I make no claim about their causes or repair.
简体中文
在本 head 复现了漏唤醒 P2 —— 3/3。只要它还在,我不建议合入本 head。
真实 Electron、本 head 新构建的 preload/renderer:按住 connecting 的快照响应 → 送达 unavailable 事件 → 再返回那个更旧的响应。3 次中 3 次都收到了事件,而五秒后仍然没有可见的输入框;重复发送完全相同的事件即恢复。注入方式:捕获 connecting 快照,100ms 送 unavailable,400ms 返回陈旧快照。单调用 preload 探针独立复现同一窗口。
被删的那行不掩盖它 —— 这是实验结论,不是论证:把删掉的 refreshShellSettings 以仅构建方式补回作对照,3 次中 3 次同样失败。该调用位于 readiness === 'ready' && isDefault 守卫之后,本处使用的 unavailable 事件在到达它之前就返回了 —— 它不在触发路径上。A/B 在两个方向上都未显示掩盖。
仍需修复的:preload 的 catch 路径仍然先 await readiness 快照,再注册 waiter。请给这个过渡加上 generation 检查(或先订阅),并补一条测试:在更旧的快照回复仍在途时送达事件。这个顺序与另一席位关于 main 进程 handler 注册与 markIpcReady 的 P3 是两回事;本轮不重审也不重新定性那条,而且补上它也不能替代这条 preload 测试。
通过的对照:恢复干净的 renderer 构建后,常规 Electron 对照全部通过(初始 unavailable → 1 个编辑器;connecting → 隐藏;等待后送达 unavailable → 恢复 1 个且可输入;最后 unavailable 重载 → 1 个)。所以常规过渡是健康的,失败的是这种交错。
边界:全新安装与依赖补丁、完整 Desktop 依赖链与 main/preload/renderer 构建通过;18 项定向测试通过;每个探针结束后自有 Electron 进程均已关闭。这些是受控 IPC 交错,不是实测的生产发生率;时序探针替换了主进程侧的身份与快照 handler,不是完整的真实 Host 故障/升级/远程传输复现。无 Windows/Linux、无完整 Desktop e2e、无全库测试、无发生率测量、未审其他 finding。本 head 上 hosted test 与 Windows package 均为已完成的失败,我不对其成因或修复作任何声明。
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
…artup Brings in recall-based search (apache#5531) and the transcript catch-up termination fix (apache#5471). The search lane moves to the recall channel and keeps the bootstrap invoke gate; bridge-contract gains the recall types alongside the handoff payload. The recall protocol declaration re-pins to epoch 169 — still a pure operation-key addition on the wire. Generated-by: Devin
The Workbar keys collapse state per Session and withRightCollapsed drops any write issued while its reducer has not activated a Session yet. applyE2eFixture ran setActiveId and immediately dispatched the collapse and openTool writes, which the reducer processed before the render-phase activate-session landed — under UI-first startup the fixture is now the first activation, so both writes were eaten and the audited surfaces (.maka-session-workbar, .maka-browser-panel) stayed collapsed forever. Set the selection before awaiting refreshSessions: the IPC round trip lets React commit the activation, and a direct setWorkbarCollapsed replaces the stale rightCollapsed snapshot + toggle, which could flip the wrong direction when read late. Generated-by: Devin
…artup Brings in named project creation and task moves (apache#5475) plus the eval comparison doc (apache#3158). addProject keeps the bootstrap invoke gate while gaining the name parameter. Generated-by: Devin
activeRuntimeHostRef awaited runtime-host-profiles:getSnapshot before installing its profiles:changed waiter, so a transition push landing inside the probe's round trip fired an empty waiter set and the read parked forever. Install the waiter first and cancel it when the probe shows the Host settled. Generated-by: Devin
…rved A runtime-host-profiles change runs a retire sweep that drops the active Session when the committed catalog lacks it. With the Host still starting, that sweep can land before the seeded fixture row reaches the catalog, retiring the Session the fixture just activated and collapsing the workbar the alignment audit waits on. Gate activation on catalog membership — the same constraint a real selection has. Generated-by: Devin
|
Fixed in Regression coverage in |
M4n5ter
left a comment
There was a problem hiding this comment.
English
Re-reviewed commit: c7762657fff435a6c36edc3ce03e5c38171ac561.
Important — The tolerant transcript timeout still blocks revision sends
session-message-settlement.ts:78–109
With tolerateOpenTimeout, the 480ms deadline calls cancelOpen() but does not settle either promise in Promise.race([opening, cancellation]).
The real preload’s cancellation callback initiates asynchronous cleanup; it does not immediately reject opening. During reconnection, the open request can still be waiting for its candidate handler. No transcript consumer exists yet, so the close request cannot cancel that wait.
Using the built production preload and actual reconnect router, I reproduced:
- Close requested at approximately 499ms.
- Settlement still pending at 700ms.
{ messages: [], settled: false }returned only after 15,019ms, at the router’s default timeout.
prepareRevisionSend() awaits this result, so edit-and-resend remains blocked by transcript readiness. The new tests miss this because their cancellation mock immediately rejects the open promise.
Please make the deadline settle independently of cleanup. Alternatively, the simpler fix is to remove this redundant transcript read from revision preparation: the normal conversation consumer already loads history. That also removes the new tolerateOpenTimeout option and its special branch.
Previous findings
- Bootstrap IPC registration gap: resolved. Calls wait for the registration pass, not Host readiness.
- WorkHub launch overlay: resolved. Shared CSS dismisses it after root content mounts; browser verification observed hidden/non-interactive state within approximately 250ms.
- Preload missed wakeup: resolved. With a stale
connectingresponse arriving after the event, bothreadyandunavailabletransitions passed three controlled runs each.
Verification and verdict
Workspace dependencies, Desktop main and preload builds passed; 78 targeted tests passed. The timing probes use controlled IPC interleavings, not a full real Host reconnection. No full Electron startup or cross-platform upgrade test was run locally.
Current CI fails the architecture ratchet: app-shell-e2e-fixture.ts grows from 637 to 750 non-trivia tokens. This is not merely a stale inventory. Windows packaging was still running at the final check.
Correctness: not acceptable as-is. Design: the startup direction is acceptable; remove the unnecessary revision-read barrier rather than extending its timeout machinery.
中文
复审提交:c7762657fff435a6c36edc3ce03e5c38171ac561。
Important — 容忍 transcript 超时的分支仍会阻塞编辑重发
session-message-settlement.ts:78–109
启用 tolerateOpenTimeout 后,480ms 到期只调用 cancelOpen(),并没有结束 Promise.race([opening, cancellation]) 中的任何一个 Promise。
真实 preload 的取消回调只发起异步清理,不会立即拒绝 opening。重连期间,open 请求可能仍在等待 candidate handler 注册,此时尚不存在 transcript consumer,close 请求无法取消这段等待。
使用构建后的真实 preload 和实际重连路由复现:
- 约 499ms 发起关闭。
- 700ms 时仍未完成。
- 直到路由默认超时,在 15,019ms 才返回
{ messages: [], settled: false }。
prepareRevisionSend() 会等待该结果,因此编辑重发仍受 transcript 就绪状态阻塞。新增测试没有发现问题,因为 mock 的取消回调会立即拒绝 open Promise。
请让 deadline 独立结束等待,清理另行进行。更简单的修法是删除 revision preparation 中这次冗余读取:普通会话 consumer 已负责加载历史,同时可以删除新增的 tolerateOpenTimeout 参数及特殊分支。
旧意见复核
- 启动 IPC 注册空窗:已修复。 请求等待注册完成,而非等待 Host ready。
- WorkHub 启动遮罩:已修复。 共用 CSS 在 root 挂载内容后撤除遮罩;浏览器检查约 250ms 后已隐藏且不拦截交互。
- Preload 漏唤醒:已修复。 在事件先到、旧
connecting响应后到的受控时序下,ready、unavailable各测试三次均通过。
验证与结论
工作区依赖、Desktop main 和 preload 构建通过;78 项定向测试通过。时序探针使用受控 IPC 交错,并非完整真实 Host 重连。本地未执行完整 Electron 启动及跨平台升级测试。
当前 CI 卡在架构增量约束:app-shell-e2e-fixture.ts 的 non-trivia tokens 从 637 增至 750,并非仅仅清单过期。最终检查时 Windows 打包仍在运行。
Correctness: 当前不可接受。Design: 启动设计方向可接受;建议删除不必要的 revision 读取屏障,而不是继续扩展其超时机制。
…-state The committed fixture carries the wait inline plus a structural dep type, which pushes app-shell-e2e-fixture past its legacyAppShell token budget (750 vs 637). Export waitForCatalogSession from the contracts module (which the ratchet does not price), collapse the workbarTab union check and the sidebarSection if-chain into a lookup, and the file lands at 623. Generated-by: Devin
prepareRevisionSend opened a second transcript consumer to verify the replica before allowing the send. The Session view's own consumer already loads history, and the write boundary is Host admission — so the read existed only to span the window until commitTranscript made the new Session the send target. With the Host reconnecting that open parks on the router's candidate wait (~15s), and the tolerateOpenTimeout escape still could not settle: cancelOpen is a noop until the open delivers its close handle, and closing it never rejects the parked invoke, so the deadline waited the full router timeout anyway (review 5263634692). The send now takes the target explicitly via SendOptions.targetSessionId (from the prepared draft), so neither the read nor the commit barrier is needed: the read, the flag, and the preparation AbortController plumbing all go away. onFollowLatest is skipped for an explicit target — a fresh Session has no reading position to prepare — and isSurfaceVisible keeps gating feedback on the committed view. The revision tests assert the preparation never opens another transcript consumer. Generated-by: Devin
|
Addressed in The The read existed only to span the window until |
M4n5ter
left a comment
There was a problem hiding this comment.
English
Re-reviewed 303c028289b145a2a0cc876f750b8ff6a9091b45.
Previous finding: resolved. Revision preparation no longer opens an additional transcript consumer. The tolerateOpenTimeout option, special timeout branch, and associated cancellation state are removed. This addresses the cause of the 15-second wait.
The explicit revision target preserves navigation checks. Targeted probes verified successful preparation, cancellation, navigation during preparation/readiness, and submission to the prepared copy.
Approved from the code-review perspective: no new Blocking or Important findings. Correctness and design are acceptable within the reviewed scope. The simplification removes redundant behavior; no further simplification is warranted here.
Validation: Desktop main/preload builds, 120 targeted tests, supplemental probes, and the strict renderer architecture check passed.
CI still fails, now because docs/astryx-surface-file-inventory.md is stale. Regenerate it with npm run astryx:surface-inventory:write and rerun CI. Windows packaging was still running at the final check. This approval does not imply that CI is green.
Full Electron startup and cross-platform upgrades were not rerun locally.
中文
复审提交:303c028289b145a2a0cc876f750b8ff6a9091b45。
上一轮问题已关闭。 Revision preparation 不再额外打开 transcript consumer;tolerateOpenTimeout、特殊超时分支及相关取消状态均已删除,从根因消除了此前的 15 秒等待。
显式指定重发目标仍保留导航检查。补充探针验证了正常准备、取消编辑、准备或 readiness 等待期间切换会话,以及向准备好的副本发送。
代码审查层面 Approve:没有新增 Blocking 或 Important 问题。 本轮范围内 correctness 和 design 均可接受。简化方案确实删除了冗余行为,没有需要继续追加的简化建议。
验证: Desktop main/preload 构建、120 项定向测试、补充探针及严格 renderer 架构检查全部通过。
CI 仍失败,本次原因是 docs/astryx-surface-file-inventory.md 过期。需执行 npm run astryx:surface-inventory:write 更新后重跑 CI。最终检查时 Windows 打包仍在运行。本次批准不代表 CI 已全绿。
本地未重跑完整 Electron 启动及跨平台升级。
session-catalog-state gained the waitForCatalogSession export. Generated-by: Devin
Command Code GO was retired upstream (apache#5545): the merge drops our invokeWhenReady gating on the deleted commandCodeLogin bridge along with the provider. The skill-draft-lifecycle spec fix (apache#5565) is test-side — waits for the slash menu to close and the disabled Skill to leave the invocable set — disjoint from this branch's send-path changes; fixtures.ts keeps both additions (testInfo plumbing, waitForSkillNotInvocable). Generated-by: Devin
The deleted E2E asserted renderer-owned contracts: a refused send keeps the draft, a retry reuses the already-prepared child instead of forking another revision, cancel restores the pre-edit draft, and a successful send retires both drafts. They now run deterministically against createAppShellRevisionActions with a stubbed bridge, plus the blocked- Skill toast through createAppShellChatActions. The transcript-handoff gate that once raced the deferred React handoff was removed upstream in apache#5494, which already covers it with a no-second-transcript-open test. The success-path draft cleanup moves out of sendWithAttachments into finishRevisionSend so the lifecycle (begin/prepare/cancel/finish) lives in one authority and stays tested. revisionCopyKey hoists to module scope so both exported attempt helpers share it; that deduplication plus reusing abandonTurnRevisionCopyAttempt inside abandonRevisionCopy keeps the file under its base token budget. Mutation-checked: removing the retry early-return, skipping the draft clear, and restoring the wrong draft text each fail their test. Generated-by: Devin
The deleted E2E asserted renderer-owned contracts: a refused send keeps the draft, a retry reuses the already-prepared child instead of forking another revision, and cancel restores the pre-edit draft text (Skill token included). They now run deterministically against createAppShellRevisionActions with a stubbed bridge, plus the blocked- Skill toast through createAppShellChatActions. The transcript-settlement gate that once raced the deferred React handoff was removed upstream in apache#5494, which already covers it with a no-second-transcript-open test. The success-path draft cleanup stays inline in sendWithAttachments — untested glue, same as before — rather than earning a new seam here. Mutation-checked: removing the retry early-return and restoring the wrong draft text each fail their test. Generated-by: Devin
* test(desktop): settle queue admissions before editing in side-chat spec The spec pressed Enter on the third follow-up and clicked edit on the first within ~100ms. beginEdit captures the queue revision at click time, so when the third entry's Host admission landed inside that window the update carried a stale expectedQueueRevision and was correctly rejected with operation_conflict. The failed commit leaves the edit textarea open, which replaces the row's queue-text span — the queue never reordered; the first entry was simply hidden behind its own edit box while the error toast reported the conflict. Wait for the third entry's edit button to become enabled — enabled marks Host-owned 'queued' state — before opening the edit, matching the spec's own note that optimistic appearance does not settle send admission. Generated-by: Devin * fix(desktop): own the native menu through its popup lifetime Two CI runs died mid-assertion after closePopup(): the main window's CDP session closed because the native menu teardown crashed the process. popupNativeMenu never retained the Menu it built, so a JS wrapper collected while its popup is open can crash the native close path (electron#20737 family). Hold each open menu until its popup callback reports it closed. The spec also closed the popup unconditionally. On Linux a popup can auto-dismiss after window resizes — this spec resizes three times first — and closing an already-dead popup hits the same teardown crash. aria-expanded tracks the popup IPC resolution, so only call closePopup while it still reports the menu open. Renderer crashes previously left no artifact evidence; the fixture now logs every page crash, including the restarted window's, into the error context. Generated-by: Devin * test(desktop): move queue row semantics to component tests The side-chat E2E asserted ComposerMessageQueue contracts through a real Electron window: the revision captured at edit click, a rejected stale-revision update leaving the row in edit mode (the misread that hid the edited row behind its own textarea), and drag reorder passing the Host-owned id list. All three are renderer-owned and now run in packages/ui against the real component with stubbed callbacks. Mutation-checked: closing edit mode unconditionally on a rejected update fails the stale-revision test. Generated-by: Devin * test(desktop): migrate skill-draft lifecycle coverage to action seams The deleted E2E asserted renderer-owned contracts: a refused send keeps the draft, a retry reuses the already-prepared child instead of forking another revision, and cancel restores the pre-edit draft text (Skill token included). They now run deterministically against createAppShellRevisionActions with a stubbed bridge, plus the blocked- Skill toast through createAppShellChatActions. The transcript-settlement gate that once raced the deferred React handoff was removed upstream in #5494, which already covers it with a no-second-transcript-open test. The success-path draft cleanup stays inline in sendWithAttachments — untested glue, same as before — rather than earning a new seam here. Mutation-checked: removing the retry early-return and restoring the wrong draft text each fail their test. Generated-by: Devin
…ent (#5571) * fix(desktop): hold the launch logo until a surface commits ready content #5494 removed the pre-mount onboarding prefetch, so AppShell's first commit lands before any data resolves. The launch overlay dismissed on that commit (`#root > *`), then the shell rendered the `.maka-onboarding-loading` skeleton card while the snapshot pull waited on the Host boot gate — the logo and the skeleton were two loading surfaces over the same gap, and the user saw logo → skeleton → app. Collapse them into one: the overlay now fades only when a surface commits `data-maka-content-ready`. The shell sets it once the first snapshot settles (the exact boundary where the skeleton used to disappear), the WorkHub composer sets it at mount, and the handoff dialog and error surface set it immediately since they cannot wait on the data they gate. With the overlay covering the gap, the skeleton card is deleted along with its styles, prop, and smoke-diagnostic selector. The 8s wedged-bundle timeout is unchanged. Generated-by: Devin Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(desktop): keep app-shell debt flat for the launch sentinel The readiness attribute grew a ratcheted file by three tokens; the two `x ? true : undefined` inert props collapse to `x || undefined` for the same value domain, and the ledger is regenerated. Generated-by: Devin Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs(desktop): regenerate the astryx surface inventory chat-message-surface no longer imports Skeleton after the launch loading surface collapsed into the launch overlay. Generated-by: Devin Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Runtime Host readiness used to be a startup precondition: boot awaited connect → handoff → managed update before creating the main window, and a separate
StartupProgressWindowcarried all progress and attention UI. A stuck handoff (thestaging→retry_requiredloop in #5476) therefore blocked the entire app behind a modal window whose only remaining action was cancel.This PR makes first paint independent of Host readiness, per the direction agreed in #5488:
main.tscreates the main window and registers all IPC first;startLocalRuntimeHostManagerstill launches immediately, but in a fire-and-forget continuation.attentionviews — the only states that need a user decision — surface inside the main window through a newruntimeHostHandoffpreload bridge andRuntimeHostHandoffOverlay. Progress views stay silent; there is no separate window.useOnboardingSnapshothydrates asynchronously behind the existing preload skeleton.cachedentries while authority is unavailable, and sends go through the durable outbox, which delivers once a ready target appears.Removals in the same change:
StartupProgressWindow+startup-presentation.ts,prefetchOnboardingSnapshot+initialOnboardingSnapshotplumbing, stale attention fields leaking intoprogresshandoff views (the view is now a discriminated union — also the source of #5476's misleadingreasondisplay), and the pre-dev.9 file-lock compatibility half-layer.Fixes #5488
Breaking change
Runtime Host builds older than dev.9 are no longer a supported update source. The legacy dir-lock / inherited-fd operator lease interop (
withLegacyFileUpdateLockLease,withRuntimeHostManagedServiceLegacyOperatorLeases, and theprocess-lifetime-lock-v1capability probe in the update path) is removed. Current process-lifetime locking, stale.supervisedmarker recovery, and the.lockdirectory wait are unchanged.Verification
npm run build:workspace-deps;npm --workspace @maka/desktop run build:mainandbuild:renderer— passnode --test --test-concurrency=4on the affected desktop suite — 117/117; onboarding snapshot suite 35/35; cli + runtime-host suites 63/63npm --workspace @maka/desktop run typecheck:stories,build-storybook,smoke:storybook— pass (380 stories / 409 theme renders, including 3 new overlay stories)npm run format,npm run lint— cleanThe surface this replaces — BEFORE rendered from
main'srenderStartupProgressHtmlwith the same attention view, AFTER is the new overlay story:Light:
Dark:
AI use
Select exactly one:
Tool(s) and scope: Devin — root-cause investigation, implementation, tests, stories, verification, and this PR description.
Checklist
Does this PR entail a change in behavior?