Repository navigation
fix(desktop): stop latest-session rows from flickering on reload, and related sidebar fixes - #1131
alfredolopez80 wants to merge 77 commits into
Conversation
hostAuthorityStore.hold returned true for every accepted hold, so a repeated failure on an already held Host re-ran blockHostAccess: it bumped the authority revision, cleared the selection again and refreshed the agent catalog on every repeated 401/403. hold now returns true only when the hold state changes (unheld to held, or uncertain to revoked). A repeated hold of the same kind still advances the Host epoch, so a verification that started before the new failure can no longer release the Host. revoked to uncertain is still refused without moving the epoch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a Host was held (a catalog or transcript read rejected with 401/403), blockHostAccess cleared the selection and the two auto-selection paths picked the most recent Host again, which was often the held one. The sidebar then flickered between the held Host and an empty selection. useAppController now exposes heldAgentRefs, recomputed on every authority revision, through NavigationContext. ChatPage's auto-select and the handleNavSelect branch with no focused chat both pick the most recent Host among the ones that are not held. With every Host held, nothing is selected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking Chat while already on the Chat route with a chat open opened a "New chat" tab of the most recent agent. nav.handleNavSelect clears the selection and returns the focused chat; handleNavSelect then reopens it through selectChatAgent, whose fast path (agent already selected, Chat route) loaded the chat with switchToChat but never selected the agent again. The selection stayed empty, and ChatPage's auto-select picked the most recent agent. The fast path now calls nav.setSelectedAgent(agentName). It runs in the same batch as the nav's setSelectedAgent(null), so no commit has an empty selection and the agent-selection effect does not re-run. For the other fast-path callers the agent is already selected and the call is a no-op. The defect came from fc3217d routing the focused chat through handleSelectChatAgent, on top of the older fast path from 7828e0f. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skipping held Hosts in the chat auto-select removes the loop that used to re-verify them by accident. Nothing checks a held Host again until the user picks it, so three cases now need to be visible: - A held Host's sessions are hidden from the sidebar with no reason given. The "Latest sessions" block now renders one row per held Host, "Couldn't verify access to <agentRef>", with a Retry button that selects the Host the same way the agent picker does. The row also renders while the session list is empty or loading. - A Retry whose access verification fails did nothing: verifyHostAccess swallowed the error. Its catch now logs the agent ref and the HTTP status (never the error message, which carries an unsanitized excerpt of the response body) and, if the authority scope did not change, shows a toast. It still returns false. - Losing access to the selected Host cleared the selection, and the chat auto-select then moved to another Host without saying why. blockHostAccess now shows a toast when it clears the selection, with separate texts for a confirmed revocation and an uncertain hold. A hold on a Host that is not selected shows no toast; the sidebar row covers it. The delete-storage feedback test that revokes the selected Host before a cancelled delete now expects the revocation toast ahead of the single "Delete cancelled" toast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every reload of the cross-agent "Latest sessions" list published the local rows first and then each Host's catalog. That first publication replaced the whole list, so remote rows already on screen disappeared until their Host's catalog came back, and the sidebar flickered on every reload. The local publication now keeps the remote rows already published for Hosts the reload is about to read, unless the reload's local index tombstoned them or a local row has the same key. Each Host's catalog page then rebuilds its remote rows from the page itself, so a row the server no longer lists goes away in the same commit as the merge. Carried rows must not outlive the authority that produced them: - An uncertain hold drops the Host's remote rows at once (hideRemoteRowsOf, called from hideHostAccess); a revocation already hides the whole Host. After a release, the Host's remote rows come back with its next catalog, not from the rows published before the hold. - A change of authority scope drops every remote row before the first publication of the new scope, where a Host can have the same name. Tests: the reload, tombstone, hold-and-release and scope-change cases in chatListPerHostPublication.test.tsx, and write witnesses for the existing #925 deletion tests. The late-ack test for a deleted chat counted only appendMessages, while sends persist through upsertMessages since a7d2e5d; it now also checks that the deleted chat receives no upsertMessages write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hanges After "Show more", the agent sessions card collapsed back to the preview whenever the list dropped to five rows or fewer with no remote page left, for example while a reload published only local rows. When the list grew again the user had to expand it a second time. The card now stays expanded until another agent is selected. The reset happens during render when the selected agent changes to a different non-null agent, because ChatThread is not keyed by agent. A null selection does not count as a change: a held Host clears the selection to null, so A -> null -> A keeps the expansion and A -> null -> B resets it. The cleanup of the session menu and the rename field when their session leaves the list is unchanged. Closes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two reads of older local messages swallowed their errors. The probe that decides whether "Load older" is shown turned a failed read into "no more history" and hid the button. The read behind the button turned a failed page into an empty one, so the click did nothing and nobody saw why. The probe now logs the failure and keeps "Load older" visible. The page read no longer catches its error: it reaches the existing outer handler, which still hides a Host on an authorization error and already logs "failed to load older messages". For other errors that handler now shows "Could not load older messages." when the request is still for the visible chat; after a chat switch it only logs. A stateful chat whose old local page cannot be read no longer continues silently to the server pages; the click reports the failure instead. Closes #1110 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d rename confirmation The renderer shares one listSessions request per Host and query for five seconds. Two events change what a cached page means, and neither dropped it. Releasing a hold. A catalog read can enter the cache while its Host is held: the cross-agent loader's read can start before the hold and fail with a generic 403 after it, and readSessionCatalog then retries with force. The first reader after verifyHostAccess released the Host shared that request. When it was rejected as revoked, the Host was held again right after it had been verified. verifyHostAccess now invalidates the Host's cached requests after a successful release, as blockHostAccess already does when it holds. Confirming a rename. Once the rename is confirmed its pending marker is removed and the server title wins the merge. A reload inside the cache window merged a page read before the rename, so the old title came back until the entry expired. attemptRenameRpc now invalidates the Host's cached requests where it removes the marker. The cache TTL is unchanged. Closes #1130 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two comments from this branch no longer said what the code does. The cross-agent catalog updater described carried remote rows by reference to the code before the carry existed. It now states the rule: remote rows are rebuilt from the page, and carried rows missing from it are dropped. The Host hold catalog cache test file described only the hold-side invalidation, while it also covers the release-side one. No behaviour change. Refs: #1130 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…button The Retry row added for a held Host had two gaps that review found. On a narrow window, Retry selected the Host but left the navigation menu open, so the recovered chat stayed under the overlay. Every other selection control in the sidebar closes the menu; Retry now does the same. It closes on click, as the session rows do: the selection handler does not return the verification result, and a failed verification still reports itself with a toast. With several held Hosts, every button's accessible name was "Retry". Each button is now named "Retry access to <host>", following the "Open <title>" names of the session rows. T-F1-1 looks the button up by its new name. New cases cover two held Hosts (distinct names, each retrying its own Host) and the mobile menu closing after Retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The final review of the held-host Retry row found three behaviours that were left over from the separate close calls in the sidebar. On a narrow window, closing the navigation menu hid the focused control and left keyboard focus nowhere visible. The five controls that navigate from the sidebar (nav items, session rows, app rows, Retry and logout) now share one close step. It closes the navigation, Settings and session options menus, and when the navigation menu was open it moves focus to the logo button that reopens it, as the Settings menu already does for its trigger. Retry therefore also closes an open session options menu. In a wide window, a successful Retry removes its row together with the focused button, so focus fell to the page body. When the Host the user retried leaves the held rows and focus is on the body, focus now moves to the Chat item. Focus that the user has moved elsewhere is left alone. Test changes: the narrow-window test stubs now restore the previous matchMedia stub. The old spy restore reset the shared stub and broke later tests under a shuffled order, and an older stub never restored its narrow viewport. The visible word "Retry" is now asserted, because the access toast tells the user to use it. New cases cover the focus destinations and the session menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…op closes it On a narrow window, the backdrop button that closes the navigation menu hid itself with keyboard focus on it, so focus fell to the page body. The backdrop now uses the same close step as the sidebar's navigation controls, which moves focus to the logo button that reopens the menu. The step is renamed closeSidebarMenus because the backdrop does not navigate. The test that leaves user-moved focus alone after a successful Retry now first shows, in the same test, that the focus effect moves body focus to the Chat item. Without that step the test also passed with the effect removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude Please do a full review of this PR and an audit of the findings it closes, and end with a clear verdict: approve or do not approve, with the reasons. Stacking. This PR is stacked on #1112. Its base branch is Full review. Check correctness, regressions, test quality and the repository rules (
Closure audit. The PR description lists what each commit fixes. For each item below, say whether the code at HEAD closes it, with
The production changes are in the Desktop renderer only ( |
|
Claude finished @alfredolopez80's task in 3m 55s —— View job PR review — stop latest-session rows from flickering, and related sidebar fixesReviewed
CorrectnessThe production surface is small and the reasoning holds up:
No regressions found in the changed code. Styling is token‑based ( Test qualitySpot‑checked the new/changed suites. Every negative assertion I inspected has a same‑test liveness witness:
The base‑failure design (each test red on its stated base; T‑925 rows green‑by‑design witnesses; the T‑925‑M3 Closure audit
"Not in this PR" — I confirm each is genuinely out of scope and not a regression introduced here: the two concurrent‑verify #849 defect, the #925 cross‑device tombstone gap, the team‑switch reload (Backend‑1), the in‑flight rename read publishing the old title once, resize not being tracked, the Settings‑popover focus fall, the overstated Minor, non-blocking notes
Neither blocks merge. VerdictApprove. (Advisory — I can't submit a formal GitHub approval.) The production changes are correct, scoped to the renderer, rule‑compliant, and each closed finding has a matching code anchor and a test with an in‑test liveness witness. Remaining gaps are explicitly carried as out‑of‑scope and are not regressions. My one caveat is that I judged the red‑on‑base and vacuity‑mutation claims from reading rather than re‑execution; the diff and the PR's documented Node‑24 run (374 files / 4034 passing) are consistent with that reading. |
Evenfire Reviewer — Round 1Reviewed head: Decision: Blocked Reviewed as the delta on top of #1112 ( Must fix
Verified
Validation gaps
PR descriptionUpdate required. The body is accurate on the flicker mechanism, but "Not in this PR" assigns the team switch to Backend-1. The case where the Host stays in Pre-existingPre-existing: none identified during the scoped review. Next actionClear carried remote rows synchronously with the team switch and add the regression described in Full review archive: — Review performed by Evenfire Reviewer |
…down A confirmed team switch changes the chat scope in the same batch as resetChat(). The chat list's scope effect strips remote rows, but as a passive effect it commits after that batch, so the commit that makes the new team current still showed the previous team's remote rows when a Host of the same name exists in both teams. resetChat() now drops remote rows through the same hideAllRemoteRows() the scope effect uses, so the switch commit has none. The new test drives handleEnsureTeamContext -> switchTeamForWorkspace and checks every committed list, with menu data loaded and before the post-paint load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Note from the PR #1112 final audit (for H11)PR #1112 adds an in-memory transcript memo. On a memo reopen whose disk page matches the remembered one, the keep branch (
H11 changes that |
|
Re #1131 (comment) (H11 and the transcript memo keep branch) The stuck Why the keep branch needs no change on this branch (
Why no new test The proposed test checks two steps. An existing test fails as soon as either step breaks:
Every mutation that would make the combined test fail already turns one of these red, so it would add no failure the suite cannot already detect. |
jozer-rami
left a comment
There was a problem hiding this comment.
Review of 62f62def4..0a5285724: two findings to fix before merge, both in the comments below. I reproduced the first and confirmed the second by mutation.
- When the selected Host is held, ChatPage switches the user to a different agent's new blank chat (
ChatPage.tsx). This is a behaviour regression compared with the base. - The App-level
heldAgentRefswiring is not covered by any test (App.tsx). If it is broken, H1 and F1 stop working and the suite stays green.
I also checked the following and found it solid:
- the H5 carry and per-Host rebuild
- the R1-H1 teardown
- H10, H11 and H16
- the #1130 invalidations, each of which is killed by its own targeted mutation
- the hold-store transitions
- scope keying of the session catalog cache
The Retry path cannot bypass a confirmed revocation: a release needs a successful server read.
When the active chat tab's Host is held (an uncertain or revoked access check), ChatPage no longer auto-selects another Host. Before, a 403 on the selected Host cleared the selection and ChatPage picked another Host and opened a blank tab for it; on a Chat click while the focused chat's Host was held, that competing select bumped the navigation epoch and dropped the held Host's successful select after verification. The controller exposes the active chat tab's Host as focusedChatAgentRef through NavigationContext. The held-Host tests mount the real controller and ChatPage for both paths, and an App test checks that heldAgentRefs and focusedChatAgentRef reach NavigationContext, changing one field per step so each memo dependency is covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/desktop-sidebar-flicker Resync the stacked PR with the head of #1112, which added f1f8f72 (retire a provisional mark superseded by a newer selection intent) and e58825b (its test). No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * commit 'e58825bf4f45aed690b3883a627c7e1f4c9f1e83': test(desktop): pin that a superseded cross-Host open leaves send admitted fix(desktop): retire a provisional mark superseded by a newer selection intent
|
@jozer-rami thanks for review 5547292745. Both findings were confirmed and are fixed in cdc29b6:
Gates on cdc29b6: Desktop build, renderer typecheck, Prettier, style rules (0 errors) and the full Desktop suite (375 files, 4039 tests, 0 failures). Not covered by this fix: the ChatPage inside the chat drawer, and a revoked Host that leaves the catalog (it then drops out of |
#1112, which this branch was stacked on, merged into dev as 81c60e3. One conflict, in attemptRenameRpc (useChatListController.ts): #1112 (d62c072) settles a confirmed rename through settlePendingRename, which stamps the settle on the catalog clock so a page requested before it keeps the local title. That covers the case 2d95a41 handled by invalidating the Host's session catalog cache on rename confirmation (#1130), so the merge takes dev's version and drops that invalidation and its test T-1130-2. With dev's version alone, T-1130-2's title assertion passed; only its witness that a new catalog request was made failed. The invalidation on Host release (T-1130-1) stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the 55 commits merged into dev since 81c60e3. No conflicts, and none of them touch files changed by this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * origin/dev: (23 commits) chore: bump control-api and control-ui versions after merging dev docs: update docs for tool-lane hook guards and Host phase scoping docs: update docs for per-Host provider non-secret env docs: update docs for dev-mode docker compose docs: update docs for runtime OAuth install callback docs: update docs for monitoring starter Makefile and webhook-proxy health port docs: update docs for team delete conflicts docs: update docs for CIMD gateway route chore: leave CLA.md unchanged pending the CLA decision test(mcp-host): use non-PEM placeholder in vertex SA fixture fix(dev-mode): keep loopback URLs out of the public boundary check fix(control-api): preserve per-phase Host hook refs on hook upgrade refactor(mcp-host): return a fresh unavailable result per guarded short-circuit fix(mcp-host): resolve provider non-secret env from per-Host env fix(mcp-host): align tool-lane hooks with LLM-lane guards and scope hooks to Host phases fix(control-ui): read OAuth install callback from control-api at runtime chore: fix stale references, CRD descriptions and dead config keys fix(external-rest-api): expose the CIMD client metadata document publicly fix(control-ui): make admin delete, budget, and hook-install copy accurate fix(monitoring): build dashboards ConfigMap from the whole directory ...
…ions The per-Host publication test builds both catalog arrival orders for two rows with the same time, but after arrival it only checked the row count and looked rows up with find(), so it stayed green with an arrival-order comparator. Assert the visible order, which is the byUpdatedDesc tie-break (Host, then chat id), in both arrival orders. Removing the Host/id tie-break from byUpdatedDesc turns one of the two cases red: expected [stateless-host, stateful-host] to deeply equal [stateful-host, stateless-host]. Refs #1123 (item 16) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e in every hold toast [R5-L2]
Only the verification toast read the last known name. A revocation that
arrived after the Host left the catalog went through the catalog name
alone and toasted the ref ("Access to agent-a was revoked."). The name
resolver now falls back to the last name a catalog of the current
principal gave the Host before it falls back to the ref, and the
verification toast uses the same resolver, read after its await.
Class-check: knownHostNamesRef.current.get( call sites -> 1 (verification toast) -> moved into principalCatalogName, 0 left; principalCatalogName( call sites -> 3 (heldHosts label, hold toast, verification toast) -> all get the map step; the label only lists Hosts still in the catalog, so the step changes nothing there; announceLoss -> 1 -> kept on the previous catalog, which named the Host
Repro-test: desktop-app/ui/src/hooks/__tests__/useAppController.heldHosts.test.tsx T-R5L2-1 (red on db60944: "Access to agent-a was revoked." instead of "Access to Research Host was revoked."; red again with the map step removed, together with T-R4L2-1 and T-R4L2-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t [R5-L4] A refresh started before a team switch is joined by the switch's own refresh, so its team 1 catalog can land after the session is on team 2. The catalog effect only kept that catalog's names out of the map; the loss loop and the selected-Host branch still ran on it. A team 2 Host it did not list was announced as "<team 1 name> is no longer available." when held, and deselected when selected. The effect now returns right after advancing the previous catalog when the catalog is not the current principal's, before any name write, loss or selection decision. Class-check: decisions in the catalog effect that read the current catalog -> 3 (name writes, held-Host loss loop, selected-Host branch) -> all behind the principal check now, previously 1 of 3; leftCatalog's previous/current userId and teamId equality -> 1 -> kept, with the current catalog the principal's it already requires the previous one to be the principal's; previousAccessCatalogRef advance -> 1 -> kept unconditional, so the next principal catalog is compared with the stale one and its team differs, which announces nothing Repro-test: desktop-app/ui/src/hooks/__tests__/useAppController.heldHosts.test.tsx T-R5L4-1 (red on acac80b: ["Billing Host is no longer available."] instead of []), T-R5L4-2 (red on acac80b: selectedAgent null instead of "agent-b"); both red again with the early return removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d [R5-M1] The chat drawer is never the stored active tab, so keepFocusedChat was false for it. When a hold deselected the drawer's Host, ChatPage auto-selected another Host and the user's next message went there. The controller now remembers the Host the hold deselected. It is kept while it is held and still listed, on the drawer (no chat tab active) or on its own chat tab, and never under the Apps picker. The marker is cleared on an authority scope reset and on the next non-null selection, so after the user picks another Host the normal auto-select applies. Class-check: keepFocusedChat readers -> 3 (App.tsx wiring, NavigationContext type, ChatPage guard) -> all read the new value, no change needed beyond the two comments; interruptedHostRef writes -> 3 (1 set in blockHostAccess on deselect, 1 clear on scope reset, 1 clear on a non-null selection) -> complete; revocation toast condition (activeChatAgentRefRef) -> 1 -> unchanged, T-A23-2 pins the picker case. Repro-test: useAppController.chatDrawer.test.tsx T-R5M1-1, T-R5M1-2 and T-R5M1-4 fail on 7fae3f6 (trail [null, 'agent-b'] instead of [null]; 'agent-b' selected instead of null) and pass here; T-R5M1-3 is a pin (green on both). Mutations: no interrupted-Host term -> M1-1, M1-2, M1-4 red; no listed term -> M1-3 and T-A9-P2 red; no selection clear -> M1-4 red; rule moved into ChatPage's mount -> M1-1, M1-2 red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T-R4L2 tests listed a Host in team 2's catalog without a display name, which the producer never does: every listed ref gets an agentDisplayByName entry (desktop-app/src/appService.ts:4290-4292). A test built on that shape could pass while a name from another scope leaked, because the absent entry already forced the ref fallback. The catalogs are now producer-shaped. Team 2 names agent-a "Team Two Host", and the assertion that pins the scope clear and the principal gate is made inside the team-switch window, before team 2's catalog lands; after it lands, that catalog's own name is expected. Class-check: T-R4L2 catalogs that were not producer-shaped -> 2 (the team-2 catalogs of T-R4L2-1 and T-R4L2-2) -> replaced; T-R4L2-3 catalogs -> 2 -> moved to producerCatalog; T-A9-T1 is outside D5 and unchanged. Repro-test: test-only commit; the rebuilt tests are pinned by mutation (they were not re-measured red on 0720afb). No knownHostNames clear on the scope reset -> T-R4L2-1, T-R4L2-2 and T-R5L3-1 red; no principal gate in the catalog effect -> T-R4L2-1, T-R4L2-2, T-R5L3-1, T-R5L4-1 and T-R5L4-2 red; verification toast reading agentDisplayByName first -> T-R4L2-2 and T-R4L2-3 red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e [R5-L6] The comment above isPrincipalCatalog cited a plan decision and promised that the team clause would be removed. It now gives the reason the clause exists: the main process builds each access catalog for the trimmed session team and records that team on it (desktop-app/src/appService.ts:4155, :4300), so a catalog whose team differs from the session team belongs to another team. Class-check: comments in desktop-app/ui/src that cite the team retirement or a plan decision for the catalog gate -> 1 -> rewritten; "Decision #6" display-name fallback comments -> 5, all from base (460fe86 and earlier), not about the gate -> unchanged. Repro-test: comment-only change; no behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[R5-L1] c96aeff trims principalTeamId, so currentTeamId (the chat-list cache scope) now carries the trimmed session team, as the delete fence and the producer already did (desktop-app/src/appService.ts:4155). The witness in the padded me.teamId case of the delete-fence test still expected the untrimmed value. The test's claim (the fence receives "team-1") is unchanged; the witness now expects the trimmed scope. Class-check: assertions on currentTeamId with a padded me.teamId -> 1 (useAppController.teamContext.test.tsx witness) -> updated; other currentTeamId assertions use unpadded ids -> unchanged. Repro-test: useAppController.teamContext.test.tsx "a padded me.teamId is trimmed" failed on 6f13133 in the full Desktop run (expected 'team-1' to be ' team-1 ', 1 failed / 4122 passed) and passes here (4/4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TRA-2] T-R5L1-1 placed the hold after the display directory had resolved, so a scope keyed by the directory team reset nothing the test could see: with authorityScope back on currentTeamId the test stayed green (36/36). The hold is now placed before the directory arrives, and the test asserts that A is held, and listed in heldAgentRefs, both before and after the arrival. The rest of the test (verification read, toast text, cached catalog identity) is unchanged. Class-check: tests that claim a state survives the post-paint directory arrival -> 1 (T-R5L1-1) -> rewritten; other directory tests assert the directory value only -> unchanged. Repro-test: with authorityScope keyed by currentTeamId, T-R5L1-1 (me.teamId="") now fails at heldHosts.test.tsx:829 (expected false to be true); on HEAD the file passes 36/36. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…R5-ASTRA-1, R5-ASTRA-3] The agents query has one cache key for every principal, so after a team switch the previous team's catalog stays cached until the new one lands. During that window ChatPage auto-select and the nav-chat blank pick could choose a Host of the previous team, and the first render of the new principal could still list a Retry row from the previous catalog. The controller now derives one current-principal catalog (same user and trimmed session team, or null) and decides holds and automatic Host choice from it. The new `selectableAgentRefs` (listed by that catalog, not held) replaces `heldAgentRefs` in NavigationContext; ChatPage and the nav-chat pick read it. While a team switch waits for its catalog the list is empty, so nothing is chosen until the new team's catalog arrives. Class-check: catalog readers in desktop-app/ui/src outside tests, 12 sites (`agentsData.agentNames` / `accessCatalog` in useAppController and `useAgentsDataController()` consumers). Decided from the current-principal catalog (5): heldAgentRefs, heldHosts labels, liveAccessCatalogRef, ChatPage auto-select, nav-chat blank pick; the catalog effect compares against the same value. Left on the raw cache (display or data only, none decides a hold or a selection): AgentWorkspace switcher (AgentWorkspace.tsx:84), FleetBoard.tsx:41, HeaderActions/index.tsx:99, AgentsPage.tsx:13, McpServersPage.tsx:148, the initialExperienceLoading boot splash, the useAgentChatController, prewarm and activity inputs, and the ChatPage skeleton/empty render. Fixture changes (4 mocks): App.chatDrawer ChatPage stub and wiring test, SidebarNav context, AgentWorkspace.chatScroll context, ChatPage.autoSelect navigation mock. T-R4L2-2 and T-R5L3-1 now expect no Retry rows inside the switch window. Repro-test: useAppController.heldHosts.test.tsx T-R5A1-1a (ChatPage), T-R5A1-1b (nav-chat), T-R5A3-1 (red on b8ee268; mutations: selectable list from raw names, nav pick from raw names, held list from raw names, each red). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gs [R5-REG-G612] The catalog effect advanced its loss baseline before it checked that the cached catalog belongs to the current principal. A catalog of the previous team that lands after a team switch therefore became the baseline. When the next team-2 catalog dropped a held team-2 Host, the baseline either did not list it or listed it under another team, so the loss toast never appeared. The principal check now runs before the baseline advance, so a foreign catalog names no Host, decides no selection or loss, and is never the catalog a later loss is measured against. The userId/teamId equality in leftCatalog stays: the first catalog of a new team is compared with the previous team's baseline and stays silent. Class-check: writers of previousAccessCatalogRef in desktop-app/ui/src outside tests, 1 site (the catalog effect in useAppController); it now advances only after the principal check. Readers of the baseline: 1 (leftCatalog in the same effect), unchanged. No fixture changes. Repro-test: useAppController.heldHosts.test.tsx T-R5G612-1 (both variants: the foreign catalog omits the Host / names it differently; red on c2584cc), T-A9-T1 extended with a later team-2 loss that must be announced (positive witness for the silent switch). Mutations, each red: baseline advance before the principal check (T-R5G612-1), no principal check (T-R5L4-1/2, T-R5G612-1, T-R5L3-1, T-R4L2-1/2), leftCatalog without the userId/teamId equality (T-A9-T1), loss toast removed (T-A9-T1 and 15 others). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t [R5-REG-F1] A hold of the selected Host marked it as the interrupted Host whatever surface had selected it. A hold made from Agents, with no chat drawer open, therefore kept a later Files drawer on the blank landing, although another Host was listed and not held. The controller now owns chatDrawerDesiredRef and App writes the drawer's open intent into it on every render. The hold marks the Host only while that intent is true. The intent, not the visibility, is used: a drawer that the window width hides is still open, and when it returns it must stay on the held Host instead of picking another one. Class-check: writers of interruptedHostRef in desktop-app/ui/src outside tests, 3 sites (blockHostAccess marker, scope reset, selection clear); only the marker changes. Readers: 1 (the surface term), unchanged. Writers of chatDrawerDesiredRef: 1 (App render). Fixture changes: the App controller mocks in App.chatDrawer, App.deepLinks and App.sandboxUiOwnership gain the ref; holdDrawerHost sets it to true after opening Files, as App does. Repro-test: useAppController.chatDrawer.test.tsx T-R5F1-1 (hold from Agents, then a first Files drawer selects B without verifying A; red with the f458ac1 marker rule at the keepFocusedChat assertion), T-R5F1-2 (hold while the width hides the drawer keeps it on A, a pin), App.chatDrawer.test.tsx open-intent wiring test. Mutations, each red: marker without the intent condition (T-R5F1-1), App without the write (wiring test), App writing the visibility instead of the intent (wiring test, at the width-suppressed assertion). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drawer keep required the Apps picker to be closed. The picker leaves the stored tab unchanged, so a drawer over Files stays mounted under it (App drawerAvailable reads the stored tab). Opening the picker therefore ended the keep, and the drawer's ChatPage selected another Host although the interrupted Host was still held and listed. The surface term now uses the same predicate as drawerAvailable: a stored tab that is not a chat. The picker guard is removed, and so is the disjunct for a focused tab of the interrupted Host, because focusedChatAgentRef already wins in that case. The picker still clears focusedChatAgentRef, so a chat tab under the picker is not kept. Class-check: readers of nav.appsPickerActive in useAppController.ts, 2 (focusedChatAgentRef, unchanged; the vm field, unchanged); the surface term was the third and is the only change. Readers of interruptedHostRef: 1 (keptChatAgentRef), unchanged. Repro-test: useAppController.chatDrawer.test.tsx T-R5F2-1 (A held in the drawer over Files, then the Apps picker opens: Files stays the stored tab, the drawer stays mounted, keepFocusedChat stays true and no Host is selected). Red on 64b56a5 at the keepFocusedChat assertion. Mutation: restoring the picker guard turns it red at the same assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the branch up to date with origin/dev, mainly for the Node.js toolchain updates. Conflicts were resolved in favour of dev: dev already contains 9b9aaef and 40e9491, the versions this branch carried. The dev test ui/src/__tests__/App.activationSeam.test.tsx failed 7/8 on the merged tree with "TypeError: Cannot set properties of undefined (setting 'current')" at App.tsx, because its controller mock had no chatDrawerDesiredRef, which App writes on every render since this branch's drawer-intent change. The mock now carries chatDrawerDesiredRef: { current: false }, as the other App test mocks do. Fixture only; no production change in this merge beyond dev. Class-check: App test controller mocks that lack chatDrawerDesiredRef: 1 (App.activationSeam, new in dev); fixed in the fixture. Repro-test: desktop-app/ui/src/__tests__/App.activationSeam.test.tsx (7 failed before, 8/8 after) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed [A29-ASTRA-1]
A conversation held in the full-screen chat tab sets no interrupted-Host
marker, because the drawer was not open (chatDrawerDesiredRef false).
After the user moves to Files and opens the drawer, App restores the last
chat tab with handleSelectChatAgent(A, { keepNavItem: true }). A is held,
so the call starts a verification without selecting A. The drawer's
ChatPage then sees no selection and no kept chat, selects B, and the
navigation epoch advances, so A's verification result is discarded.
When a held Host is requested with keepNavItem and nothing is selected,
the drawer is showing that Host's conversation. handleSelectChatAgent now
records it as the interrupted Host before the verification, as a hold
inside the drawer does, so keepFocusedChat holds and ChatPage waits for
the verification.
The test harness renderChatActionConsumer gains rerenderController().
App owns the drawer state next to the controller, so a drawer state
change re-renders the controller in the same batch; a test surface with
its own state does not, and calls rerenderController() to model that
render.
Class-check: handleSelectChatAgent calls with keepNavItem: 10 (App.tsx 9, useAppController openChat-in-drawer 1), all drawer surfaces. Three pass vm.selectedAgent and never reach the branch with nothing selected. The fix is in the one held branch they share. The Agents-hold case of R5-REG-F1 does not call it; T-R5F1-1 stays green.
Repro-test: desktop-app/ui/src/hooks/__tests__/useAppController.chatDrawer.test.tsx T-A29A1-1 (verified, denied): 2 failed before ("expected 'agent-b' to be null"), 2 passed after; mutation `if (false)` on the new branch: red, reverted and checked by sha256.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…A-2]
T-F3-2 (generic 403 and host_access_revoked) and T-R5L4-1/2 asserted
that no toast appears, with no toast from the same emitter in the same
test, so a dead emitter passed them as well as a correct guard.
T-F3-2 now holds the selected Host afterwards and expects exactly its
hold toast (Could not verify access / Access was revoked). The catalog
uses producer-shaped display names, so the negative filter matches the
display name as well as the ref, and matches "access" case-insensitively
because the revoked toast starts with "Access".
T-R5L4-1/2 now land team 2's own catalog listing B, then one without
B, and expect exactly one named loss toast for B after that catalog
(helper expectTeamTwoLossOfB, which reads only the toasts committed
after the drop).
Class-check: no-toast assertions in useAppController.heldHosts.test.tsx without a same-emitter positive phase: 4 (T-F3-2 x2, T-R5L4-1, T-R5L4-2), all given one. Production code unchanged.
Repro-test: desktop-app/ui/src/hooks/__tests__/useAppController.heldHosts.test.tsx 41/41; mutation `if (false)` on the hold-toast condition in blockHostAccess: T-F3-2 2 failed ("expected [] to deeply equal [ Array(1) ]"); mutation removing pushToast from announceLoss: T-R5L4-1/2 2 failed (same); both reverted and checked by sha256.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…A29-ASTRA-1, A29-ASTRA-2] The renderer tsc check failed on the two previous commits: the drawer test's placeholder callbacks threw, so TypeScript inferred `() => never` and rejected the reassignment, and T-F3-2 typed its channel as `string` where the IPC fixtures take `RpcChannel`. Vitest does not type-check, so both suites were green. Class-check: tsc errors in files touched by 6017ac9 and d57b3fb: 3 (chatDrawer.test.tsx:976, :1010; heldHosts.test.tsx:408), all fixed with explicit types; no other file reported. Repro-test: `npx tsc -p ui/tsconfig.json --noEmit` UI_TSC_EXIT=2 before, 0 after; useAppController.chatDrawer.test.tsx 18/18, useAppController.heldHosts.test.tsx 41/41. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st is requested [A31-SOL-1] 6017ac9 set the interrupted-Host marker on every drawer request for a held Host made with nothing selected. With A kept in the drawer and B held in the background, requesting B from the drawer's switcher (denied) moved the marker to B; when B then left the catalog the keep ended and ChatPage selected another Host, abandoning A. A request is not a selection, so the drawer request now sets the marker only when none is set; a selection or a scope reset still clears it. Class-check: writes of interruptedHostRef in useAppController.ts: 4 (scope reset :426 and selection effect :431 clear it; blockHostAccess :443 sets it only for the selected Host, after a selection that already cleared any marker; handleSelectChatAgent :1325 is the one that could replace a live marker, now guarded). Repro-test: T-A31SOL1-1 in useAppController.chatDrawer.test.tsx red on 9619012 (:756 keepFocusedChat expected false to be true), green after (19/19); mutation removing `interruptedHostRef.current === null` turns only T-A31SOL1-1 red (18/19), reverted and checked by sha256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2-SOL-2] A hold clears the selection to stop reads. The full-screen chat keeps the Host it shows in its tab; the drawer kept it only through an inferred interrupted-Host marker with four writers and a selection-driven clear, plus the drawer open intent written by App. That marker could outlive the conversation it described: with the drawer closed, ChatPage is unmounted, so no selection cleared a marker whose Host had left the catalog, and on reopen the restore of a held last tab was refused (SOL-2). The drawer's Host is now the Host of the last chat selection (lastChatHostRef), written by every chat selection and by a drawer request for a held Host while the drawer does not already show another held, listed Host. Over a non-chat tab, keepFocusedChat reads it; the keep rule itself (held and listed) is unchanged. Contract change, accepted: a hold made in Agents after a chat selection of that Host now keeps it when the drawer opens over a non-chat tab, because the drawer shows the last chat conversation. Class-check: writes of lastChatHostRef in useAppController.ts: 3 (selectChatAgent entry :1187, every chat selection; handleSelectChatAgent held branch :1317, a drawer request for a held Host with nothing selected and no held, listed Host already recorded; authority scope reset :424, null). Removed: the marker write in blockHostAccess, the clear effect on nav.selectedAgent, chatDrawerDesiredRef (vm export and the App render write). Test fixtures: the chatDrawerDesiredRef mock line removed from App.activationSeam, App.sandboxUiOwnership, App.chatDrawer and App.deepLinks (1 each); 7 intent writes removed from useAppController.chatDrawer.test.tsx (on 2692891 the marker depended on that write, so the red-on-HEAD run kept it; the controller no longer reads it); the App wiring test "gives the controller the drawer open intent" deleted with the write it pinned. Repro-test: T-A32SOL2-1 and T-A33-F1 in useAppController.chatDrawer.test.tsx red on 2692891 (trail=["agent-c"] expected 'agent-c' to be null; keepFocusedChat expected false to be true), green after (7 affected files, 190/190). Mutations, each reverted and checked by sha256: no write in selectChatAgent turns 8 drawer tests red, T-A33-F1 included; the "held and listed" clause removed turns T-A31SOL1-1 red; the held-branch write removed turns T-A32SOL2-1 red (the ASTRA-1 tests stay green: their Host was already recorded by its chat selection); the reader without the drawer Host turns 10 tests red, T-R5M1-1 included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[A33-SOL-P2] A chat selection records its Host as the drawer's Host. Closing that Host's last chat tab dismissed its conversation but left the record, so a drawer opened later over Files kept the held Host and showed a blank landing instead of selecting a listed Host. The tab strip now tells the controller which tab it closes and what remains, before it commits the close. When the closed tab was the last chat tab of the recorded Host, the record is cleared. It is cleared there and not in an effect, because ChatPage's effect runs first and would read the old value. Other chat tabs of that Host keep the record. T-A33-F1 gains a positive phase on the same mounted drawer, so its "no other Host selected" half fails when ChatPage never auto-selects. The ChatPage comment names the Host it keeps and when the keep ends. Class-check: production callers of closeWorkspaceTab: 1 (App.tsx handleCloseWorkspaceTab; the close shortcut calls the same handler); reconcileWorkspaceChatTab removes no tabs. Writes of lastChatHostRef: 4 (selectChatAgent entry, held branch of handleSelectChatAgent, authority scope reset, forgetClosedChatHost). Test fixtures: forgetClosedChatHost mock added to App.activationSeam, App.chatDrawer and App.sandboxUiOwnership (1 each). Session deletion with its chat tab open is not exercised. Repro-test: T-A34-1 in useAppController.chatDrawer.test.tsx (the review's scratch journey: red on 9997d58, keepFocusedChat expected true to be false); green after. Mutations, each reverted and checked by sha256: early return in forgetClosedChatHost turns T-A34-1 and T-A33-F1 red; an unconditional return in ChatPage's keep guard turns T-A33-F1 red; removing the App call turns the activationSeam close assertion red. Desktop vitest 386 files / 4521 tests, both tsc, verify:electron: EXIT=0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents [A34-SOL-P3] The keep comment in useAppController and the chat-drawer describe comment still described the drawer Host as the Host a hold interrupted, or only as the last chat selection. Both now name the accepted rule: the Host of the last chat selection or of a drawer request for a held Host, until the user closes that Host's last chat tab. Comments only. Class-check: stale comments describing the drawer keep; 2 sites (useAppController.ts keep block, useAppController.chatDrawer.test.tsx describe), both fixed; no reference to interruptedHostRef or chatDrawerDesiredRef remains. Repro-test: none (no behaviour change); Desktop vitest 386 files / 4521 tests EXIT=0, both tsc EXIT=0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en [A36-R1]
While a Host is held, the chat tab or drawer keeps its conversation on
screen and Latest sessions shows a Retry row for it. Retry selected the
Host with no chatId, so a successful verification opened a new blank
chat for it instead of the conversation the user was reading. The Chat
nav click already passes the focused chat's chatId.
The held-Host entry of the kept Host now carries the chatId of that
conversation: the focused chat tab's, or for the drawer the last active
chat tab of that Host, which is the tab the drawer restores. Retry passes
it, so a successful Retry activates the existing tab.
Class-check: production consumers of heldHosts: 2 (SidebarNav Retry row and its focus effect; App passes it through NavigationContext). Retry is the only onSelectChatAgent call that reads heldHosts. Only the kept Host gets a chatId; other held Hosts keep {agentRef, label}. Test expectations updated for the added field: 8 exact heldHosts assertions (7 in useAppController.heldHosts.test.tsx, 1 in useAppController.chatDrawer.test.tsx), each with the chatId of its kept tab.
Repro-test: T-A36R1-1 in useAppController.heldHostRetry.test.tsx (real controller, ChatPage and SidebarNav): red on 0434707 (active tab chatId expected 'chat-1', received null); green after. Mutation: Retry without chatId turns it red at :124, reverted and checked by sha256. Focused files 4 / 83 tests and renderer tsc: EXIT=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…36-R2]
The chat drawer takes its Host from the last chat selection, written at
the entry of selectChatAgent. Opening a notification full screen selects
the Host through its own fast and pending-selection paths, which never
wrote it. After the user went back to Files the drawer showed that Host,
but a hold of it ended the keep and ChatPage selected another Host.
The notification-open path now records its target Host after the drawer
branch, which already goes through handleSelectChatAgent.
Class-check: non-null nav.setSelectedAgent writers in useAppController.ts: 4. Two are inside selectChatAgent/handleSelectChatAgent (already recorded by W1); openAgentWorkspace (:1167) opens the Agents workspace, not a chat, and stays unrecorded (F1, T-R5F1-1); the notification-open path (:1469) is the one fixed here. The doc comment of lastChatHostRef ("written by every chat selection") is now true.
Repro-test: T-A36R2-1 in useAppController.notificationDrawer.test.tsx (real controller, ChatPage as the drawer over Files): red on 4a1a82d (keepFocusedChat expected true, received false, :110, with B held); green after. Mutation: remove the write turns it red at :110, reverted and checked by sha256. Focused files 4 / 65 tests: EXIT=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t [A36-R3] T-F2-2 checks that a verification which fails after a team switch shows no toast. It rejected only the team-1 display name, but after the switch the toast would name the Host by its team-2 name or its ref, so the test stayed green with the scope guard removed. It now compares every "Could not verify access to" toast before and after the rejection. Class-check: negative toast assertions on verifyHostAccess failures that match one display name: 1 (T-F2-2). T-F2-1 asserts the exact toast list. Repro-test: T-F2-2 in useAppController.heldHosts.test.tsx. Mutation M09 (scope guard in verifyHostAccess replaced by `if (true)`): was green on 0434707, now red (received ["Could not verify access to agent-b."]); reverted and checked by sha256. File 41/41: EXIT=0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keepFocusedChat doc now names the kept Host over a non-chat tab (the last chat selection or drawer request). The resetChat comment says the signed-out session load can call it with the scope unchanged. The catalog-effect comment says the loop above announces a held Host's loss. Class-check: comment-only edits at 3 sites (NavigationContext keepFocusedChat doc, useAgentChatController resetChat invalidation, useAppController catalog effect); no behaviour change. Repro-test: none (comments); focused suites 4 files / 67 tests EXIT=0, renderer tsc EXIT=0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the branch up to date with origin/dev so the T2 lane's ancestry check passes. No conflicts; dev's 36 commits touch control-api, control-ui, charts and docs, not desktop-app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@zach88 thanks for round 5. I took your advice and wrote the design note before touching the code. All six findings were reproduced on The design note The note reduces the round to four structural facts. Each one became one change:
The decision table in the body now has the drawer column and rows 11 (leaves, then revoked) and 12 (scope change only through the directory team). Must resolve
Optional
None of the optional items needed a separate issue. After round 5
Validation on
Details, test ids and the re-measured mutation table are in the updated PR body. |
Brings in 4faf305 (ci: pin the build/test job to ubuntu-24.04 for Playwright). ubuntu-latest now resolves to Ubuntu 26.04, where Playwright 1.58.2 cannot install chromium, so the desktop-app and external-rest-api build/test jobs failed at `npx playwright install`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * origin/dev: docs(nginx-egress-proxy): cover nginx-unprivileged and the verifier test in the bump steps test(nginx-egress-proxy): run the TLS proxy with the generated securityContext fix(nginx): compare NGINX_VERSION per platform as a whole JSON value fix(nginx): reject glob tags, tagged newName, COPY --from and YAML node properties ci: pin the build/test job to ubuntu-24.04 for Playwright fix(test): skip nested live E2E packages in T0 ci(nginx): pull NGINX through mirror.gcr.io and bound the NGINX jobs fix(nginx): verify digests under their full name, with an optional mirror fix(nginx): one grep scanner and a shared release policy for the image guard fix(nginx): make verify-nginx-image-digests.sh executable fix(nginx): require fixed NGINX releases and wait for TLS test log lines fix(e2e): drop one-iteration namespace loop flagged by shellcheck SC2066 fix(nginx): upgrade platform NGINX images to 1.30.6 for October 2026 advisories
|
Took another pass at What's left is in the drawer keep and in the team-switch catalog, and most of it comes from one split. The Host of "the conversation on screen" lives in Verdict: 🔴 Not ready to approve
This is my read from the review, not a formal GH approval. Before another patch, I'd really write the short spec this time rather than a note. It should define one record
Items 1, 2 and the optional item 7 all fall out of that table. Item 3 belongs in it too. Must resolve before approval
Optional (non-blocking)These can land as follow-ups or at your call. None of them blocks the merge. Item 7 is a Medium that should fall out of the same spec as items 1 and 2.
What checked out clean
|
Brings in #1178 (control-api: return secret identity in the mcp-secrets list). Two control-api files; no overlap with this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(desktop): stop latest-session rows from flickering on reload, and related sidebar fixes
Base:
dev. This branch was stacked on #1112, which merged intodevas81c60e35a;devis merged in at046e2894e, atfab73b054(57a86a470), atf92106d69(ca5f0c91f), at06fc32acb(bffb8f372), at523f328e0(ce1b21abf) and atd2e4cc57c(899a76016). Closes #1109, closes #1110, closes #1130, closes #1152. Refs #1123 (item 16). Related: #849, #925, #941.Since the merge of
devat523f328e0, the diff againstdevtouches onlydesktop-app/(36 files). The two commits outside the Desktop code,fb1cb2a96(Minikube T1 private-suite exclusion) and154442d00(mcp-host fixture copy for Node 24.22), stay in the branch history, butdevnow has the same fixes (40e94910b,9b9aaefe1), so they no longer appear in the diff (R2-L3, R3-L3).Summary
74ca2d0d6holdis idempotent for the same kind (H2): a repeated hold still advances the epoch but no longer re-runsblockHostAccess(catalog refresh, selection clear).16f30000bChatPageand the no-focused-chat branch ofhandleNavSelect.a1d6a4d9c57f03ac4200ae473d5ac4d2f3794033b4c202d95a4180dev; see the #1130 section.9edf68d262033c8616ff7899b188df66b51d0a5285724resetChat) drops remote latest-session rows. Before this, the commit that made the new team current still showed the previous team's remote rows for one commit, because the scope effect that strips them runs after that commit (Evenfire Reviewer R1-H1; T-R1H1-1, T-R1H1-2).cdc29b6c2ChatPagedoes not auto-select another Host, so the focused chat tab stays until Retry or a Chat click verifies the Host (review 5547292745; T-J1-1, T-J1-2, and an App wiring test inApp.chatDrawer.test.tsx).87212376fandce9e43790extended the guard to a Host that left the catalog;44531cd74returns it to held Hosts only, and2d9fed46bto held Hosts that still have a Retry row; see "Keeping the chat tab".046e2894edev(81c60e35a). One conflict, inattemptRenameRpc, resolved withdev's version (see #1130).fab73b054dev(57a86a470). No conflicts.b8b1232d587212376f<name> is no longer available.(name from the previous catalog of the same user and team). Found by the real-app E2E (F3a, F3b). The deferred uncertain-hold toast and the absent-Host guard inChatPagethat this commit also added were removed by44531cd74.f8a9daff1,ad8d013fe,3ab0d876187212376f(A9 review, external review r3). Superseded:44531cd74removed the deferred toast, so the cases they covered no longer exist.f92106d69dev(ca5f0c91f). No conflicts.ce9e43790ChatPageauto-selects again when the open tab's Host is not in the new team (R1-H1; T-R1H1-T stays as its regression test). Its per-hold-episode toast fix (R1-M1) was removed with the deferred toast in44531cd74. Review 6271309203.6e947f319resetChatdiscards in-flight list reads of the previous scope at once, so a previous-team page that settles during the switch cannot reach the new team's first commit (R1-M2).6b25f9a1f06fc32acbdev(bffb8f372, #1056). No conflicts in this PR's files.154442d00loadDevStorecopies the dev GFS store fixture entry by entry. Node 24.22.0 (nodejs/node#64124) makesfs.cpSyncwitherrorOnExistthrow when the destination directory exists, which broke 11 mcp-host tests on the CI runner.devnow has the same fix (9b9aaefe1), so it is no longer in the diff.44531cd74Could not verify access to <name>.at once.ChatPagekeeps the focused chat tab only while its Host is held. The Retry row names each held Host by its display name (#1152). This removes the deferred toast,HostAuthorityStore.heldSinceandholdKind(), the catalog-returningrefresh(), the pending-toast marker and the per-team listed-Hosts set.fb1cb2a96tests/contracts/password-recovery-suites.jsonas public CI does. Without it T1 refuses the run withZERO_TESTS_EXECUTED(3 pending tests). Cherry-picked from #1047 (e842b4427).devnow has the same fix (40e94910b), so it is no longer in the diff.2d67ff2a7,555ed4804,d827635de,2a150797eheldHosts(R2-M2); the SidebarNav context mock carriesheldAgentRefs(R2-L5); H5 test comments state the reason instead of a spec section (R1-L7); T-A9-P1-4 is restored with the immediate toast (R2-L2).2d9fed46b<name> is no longer available.(name from the previous catalog), andChatPageselects a listed, non-held Host (R2-M1; T-A9-P2).142880262Access to <name> was revoked.even when an earlier uncertain hold already cleared the selection (R2-L1; T-R2L1-1, T-F3-2).44013d2599c326b615d477e26f8useAgentChatController.ts:3677-3679, formatting only (R4-L6).11b12c476keepFocusedChatdoes not hold a tab that is not on screen; thekeepFocusedChatcomment inNavigationContext.tsxstates the current rule (A22 review). It also silenced the loss and revocation toasts behind the picker;e0e470707and5fd232e07reverse that part.29416e8d1e0e470707<name> is no longer available.once, at the catalog change, whatever is focused. Returning to Chat does not repeat it (R4-M1; T-A23-1, T-R4M1-2).5fd232e07focusedChatAgentRefforkeepFocusedChat(R4-M1; T-A23-2).762bc96070e248b3c57346d8bf411146a14einvalidateAuthorityScopecomment names every continuation the scope generations cut, including the tails of create, rename and delete (R4-L5); the T-F3-1 title regains its space (R4-L6).0720afb48c96aeff2eme.teamId, trimmed as the main process trims it), not by the directory team. Before, whenme.teamIdwas empty, the team directory arriving after paint changed the scope with no principal change and cleared the holds and the last known names, so a failed Retry of a Host still in the catalog saidCould not verify access to <ref>.(R5-L1; T-R5L1-1).db6094475acac80bcfAccess to <last known name> was revoked.instead of the ref (R5-L2; T-R5L2-1).7fae3f6e9<name> is no longer available.nor deselects a Host of the new team (R5-L4; T-R5L4-1, T-R5L4-2).eefce7b6eChatPageauto-selected another Host right after the hold, because the drawer is never the stored active tab (R5-M1; T-R5M1-1..4).27e3eaa336f1313342d502a829auseAppController.teamContext.test.tsxexpects the trimmed session team thatc96aeff2eintroduced (R5-L1 follow-up).b8ee26894c2584cc27ChatPageauto-select and the Chat nav pick read one catalog: the cached one only when it belongs to the current user and team, else none. During a team switch the previous team's cached catalog no longer letsChatPageor the Chat nav pick a Host of the previous team, and the first render of a new principal shows no Retry row from the previous scope (R5-ASTRA-1, R5-ASTRA-3; T-R5A1-1a/b, T-R5A3-1).f458ac18f64b56a5b69498bc661523f328e0dev(ce1b21abf). The conflicts were in the mcp-host fixture and the Minikube T1 scripts, and were resolved withdev's version, which already has both fixes. ThedevtestApp.activationSeam.test.tsxgains the controller field this branch adds to its mock (fixture only).6017ac9a1ChatPageselected another Host during the verification and the restore was discarded (A29 diff review).d57b3fb90,961901294tsc(A29 diff review).2692891d59997d58b4.9997d58b4f550f0fe8,0434707334a1a82dda8348f9df422b1d559dCould not verify access totoast after the switch, so removing the scope guard turns it red (blind review).e8615977ed2e4cc57cdev(899a76016). No conflicts in this PR's files.What this PR claims about the flicker cause (step 0)
The flicker is visible only when Latest sessions contains
remote: truerows, that is, sessions created on another machine. The user who reported it runs two clients (macOS and Debian).index-BWtzYAD0.js): 4 minutes of use with 10 logpoints attached recorded 0 holds, 0 verifications, 0 Latest-sessions reloads and 0 401/403 errors in the main log (counts={}). A 3-minute witness recording on the same logpoints captured 5navand 3autoselectevents matching the user's clicks, so the logpoints were live. On that account the H1 trigger did not appear. This neither confirms nor rules out H1 for the reporter, because the trigger may depend on their account or team (host access: return a machine-readable revocation code on the RPC token mint 403 #941).So this PR does not claim that H1 causes the flicker. H1 is a defect that can produce it:
blockHostAccessholds it, clears the selection and refreshes the agent catalog.ChatPagepicks withpickLatestAgent, which ignores holds, and can pick the held Host again.handleSelectChatAgentverifies the held Host with an uncachedlistSessions(limit: 1). If it succeeds, the Host is released, the release detector bumpslatestReloadRevision, and the cross-agent loader runs again. Its local-only publication is the flicker (H5).The loop needs three conditions that have not been observed in production: a failing read, a succeeding
limit: 1verification right after it, and the held Host being the onepickLatestAgentpicks. The Vitest tests inject the trigger (T-H1-3), so they prove the mechanism, not that the trigger occurs in production.H5 removes the flicker of rows of Hosts that stay in the list on any same-scope reload, whatever starts the reload. H1 and H2 are fixed regardless of the trigger: an auto-selection must not pick a held Host or start verifications without a user action, and a repeated hold must not repeat the refresh.
H5 does not cover two cases: a catalog refresh during a temporary team switch can drop a non-held Host from
agentNames(its rows leave and return; handled in Backend-1), and a non-authorization catalog failure removes that Host's remote rows on purpose, as today.Why the #849 holds stay
Opus 5.5 and GPT-6 Astra reviewed separately whether to remove the host holds; both concluded keep-with-changes.
latestReloadRevision, and every new hold refreshes the catalog.30ecd0dfe).The actual hiding contract. The message of
54c4919ddsays "hide only on confirmed revocation", but the code also hidesuncertainholds:visibleLatestChatSessionsfilters every held Host (useChatListController.ts:1668-1678atba75871a8). This PR keeps that contract and makes it visible (F1).Visible changes (F1–F3)
Fixing H1 removes an accidental recovery: today the auto-selection re-picks a held Host and its verification releases it. After this PR nothing re-checks a held Host until the user selects it, opens one of its notifications, or the scope changes. Three consequences, handled as the user chose:
F1. Latest sessions shows one row per held Host that the catalog still lists, before the session list:
Couldn't verify access to <name>with aRetrybutton, where<name>is the Host's display name, as in the toasts (Desktop: the Retry row names the Host by agentRef while the toast that points to it uses the display name #1152). It also renders while the list is empty or loading.Retrydoes what a click in the agent selector does (handleSelectChatAgent(agentRef, { selectLatest: false })) and closes the sidebar menus, as the session rows do. Each button's accessible name isRetry access to <name>. Display names are not guaranteed distinct, so two held Hosts with the same display name get the same row text and button name. The toasts and the agent switcher (AgentWorkspace.tsx:286) use that same name and do not tell them apart either (R2-M3, closed in writing).Focus. On a window 900 px wide or less the sidebar is an overlay menu. The five controls that navigate from it (nav items, session rows, app rows, Retry, logout) and the backdrop that closes it now share one close step: it closes the navigation, Settings and session options menus, and when the overlay was open it moves focus to the logo button that reopens it. In a wide window, a successful Retry removes its row with the focused button; focus then moves to the Chat item, unless the user has already moved it elsewhere.
F2. A failed verification logs
console.warn('[host-authority] access verification failed', { agentRef, status })with the HTTP status only (the error message carries a response excerpt that is not scrubbed), and shows the toastCould not verify access to <name>.when the scope did not change.F3. One toast per event. A revocation followed by the catalog drop of the same Host is two events and shows both toasts, in that order (R4-L1, pinned by T-R4L1-1):
Access to <name> was revoked.Could not verify access to <name>.The toast makes no Retry promise, because the hold's catalog refresh can drop the Host, and the Retry row lists only Hosts the catalog still has.Access to <name> was revoked.even when the Host is no longer selected (for example after an uncertain hold of the same Host, R2-L1), and also while the Apps picker is open over that tab (R4-M1).<name> is no longer available., with the name from the previous catalog of the same user and team.ChatPageauto-select and the Chat nav pick read the same current-principal catalog, so while such a catalog is the cached one none of them lists or picks its Hosts (R5-ASTRA-1, R5-ASTRA-3). Display surfaces (the agent switcher, Fleet, the header, the Agents page) still list the cached catalog; see "Not in this PR".Keeping the chat. While the Host the user was on is held and still in the catalog, that is, while it has a Retry row,
ChatPagedoes not auto-select another Host, so the next message cannot go to a different Host. What stays is the chat surface: the selection is cleared, souseAgentChatControllerclearsactiveChatIdand the messages until Retry or a Chat click verifies the Host. On the full-screen chat tab the kept Host is the tab's Host. The chat drawer (over Files, Settings or an app) is never the stored active tab, so the controller records the Host of the conversation it shows: the Host of the last chat selection, of a drawer request for a held Host, or of a chat opened from a notification. The record ends with the next chat selection, with a team or login change, or when the user closes the last chat tab of that Host. While that Host is held and listed, the drawer stays on its blank landing and selects no other Host (R5-M1, A32-SOL-2, A33-SOL-P2, A36-R2). A successful Retry returns to the conversation on screen, the focused chat tab's or the drawer's, instead of opening a blank chat for the Host (A36-R1). When the held Host leaves the catalog, its Retry row goes with it and nothing is kept:ChatPageselects a listed, non-held Host. Holds reset with the authority scope, so after a team switch no Host is held andChatPageauto-selects a Host of the new team. While the Apps picker is open no chat is focused, sokeepFocusedChatkeeps nothing; the picker affects only this rule, not the toasts."Tab" is the stored active chat tab, and "Picker" the Apps picker open over it. "Drawer" is the chat drawer over Files, Settings or an app. "Elsewhere" is Files, another chat, or no tab. "Same principal" means a catalog of the current user and team.
ChatPagepicks a listed Host<previous name> is no longer available.ChatPagepicks a listed, non-held Host<previous name> is no longer available.once, at the catalog change; not repeated on returnChatPageauto-select and the Chat nav pick ignore it (R5-ASTRA-1); display surfaces still list it ("Not in this PR")Could not verify access to <name>.Access to <name> was revoked.Access to <name> was revoked.Could not verify access to <last known name>.keepFocusedChatChatPagereads it until the drawer opensAccess to <last known name> was revoked.when row 5 or 6 applies, else noneRecorded, not changed: in the drawer, New chat after a hold leaves the blank landing as it is (with no Host selected it has nothing to open). Reopening the drawer restores its last chat tab through
handleSelectChatAgent, which for a held Host is a verification, like Retry: it ends the keep if it succeeds and keeps the drawer on that Host if it fails.If #941 makes every Host return 403, no Host is selected and each one gets a Retry row; every failed Retry shows a toast. The app does not retry on its own.
H16: Chat opens the most recent agent
Found by the step 0 recording: at 17:03:36 UTC a click on Chat while in an
alfredo-devchat producednav→select alfredo-dev fastPath:true→ 4 ms laterautoselect pick=pr-reviewer→select pr-reviewer chatId=null.nav.handleNavSelect('chat')clears the selection and returns the focused chat;handleNavSelectcallshandleSelectChatAgentwith it; on the Chat route with that agent selected,selectChatAgenttakes the fast path, which loads the chat but never setsselectedAgentagain. With the selection empty,ChatPageauto-selects the most recent agent and opens a "New chat" tab for it.fc3217d04(2026-09-17, v0.10) madehandleNavSelectload the focused chat throughhandleSelectChatAgent; the fast path (7828e0fb4) is older. Each piece is correct alone.nav.setSelectedAgent(agentName)in the same batch, soChatPagenever sees the empty selection. On every other fast-path caller it is a no-op.H11 (#1110): trade-off
hasLocalMessagesBeyondnow logs[chat-history] failed to probe older local messagesand returnstrueinstead of swallowing the error, so "Load older" stays visible.handleLoadOlderMessagesno longer turns a failed local page read into[]; the outer catch logs it and showsCould not load older messages.if the chat is still the one in view.Accepted cost: today, when an older local page cannot be read in a stateful chat with server turns, loading silently continues from the server. After this PR that click shows the toast and loads nothing. A corrupt local copy is a defect the user or support must see; a silent fallback would hide it.
Two consequences of where the change sits, both intended:
handleLoadOlderMessages, so it also appears when the server backfill orreplaceMessagesfails in that click, not only when the local page read fails. After areplaceMessagesfailure the merged page is already on screen.truefor any rejection of its local read, including a failed index read or legacy migration insideloadMessages. "Load older" stays, and the next click reports the error.#1130: the session catalog cache
The renderer shares one
listSessionsrequest per Host and query for five seconds. Two events change what a cached page means, and neither dropped it:readSessionCatalogretries withforce. The first reader afterverifyHostAccessreleased the Host shared that request; when it was rejected as revoked, the Host was held again right after it had been verified.verifyHostAccessnow invalidates the Host's cached requests after a successful release, asblockHostAccessalready does when it holds.2d95a4180invalidated the Host's cached requests where the marker is removed. perf(desktop): reduce chat-open latency and keep reopened transcripts in memory #1112 fixed the same case indev(d62c072bf):settlePendingRenamerecords when the rename settled on the catalog clock, and a page requested before that, cached or in flight, keeps the local title. The merge ofdevtakes that version and drops this PR's invalidation and its test T-1130-2. Withdev's version alone, T-1130-2's title assertion passed; only its witness that a new catalog request was made failed.The TTL is unchanged. A hold alone does not re-run the cross-agent loader (
isHostAccessBlockedis a stable callback), so T-1130-1 uses the forced-retry path to put a during-hold request in the cache.Pre-existing fallbacks found, not changed here
[]with a warning (useAgentChatController.ts:1795-1801,:2174-2180atba75871a8).[]with no warning (useChatListController.ts:1514-1520atba75871a8).switchToChatrecomputeshasOlderMessagesfromcached.length; after a "Load older" that value is no longer the number of loaded local messages, so the button can hide while history remains. Reported separately.Tests
The repository tests are Vitest. The real-app E2E in the next section runs outside the repository. Each claim is tested at the level where the defect lives. "Base" means the new and modified tests run against the base production code:
48fcd0b30for the rows up to T-925, re-checked on45f1584c1after the first rebase, and45f1584c1for T-1130.ui/src/lib/__tests__/hostAuthorityStore.test.tsui/src/pages/__tests__/ChatPage.autoSelect.test.tsxui/src/hooks/__tests__/useAppController.heldHosts.test.tsxui/src/hooks/__tests__/useAppController.workspaceTabs.test.tsxui/src/components/__tests__/SidebarNav.test.tsx9edf68d26)2033c8616)Retryff7899b18)ui/src/hooks/__tests__/useAppController.heldHosts.test.tsxui/src/hooks/domain/__tests__/chatListPerHostPublication.test.tsxbyUpdatedDescturns one case redui/src/components/agents/__tests__/ChatThread.sessionList.test.tsxui/src/hooks/domain/__tests__/switchToChat.unified.test.tsxloadMessagesdoubleui/src/hooks/domain/__tests__/chatListTitleMerge.test.tsx(existing, witness added)src/__tests__/chatStore.test.ts(existing, witness added)ui/src/hooks/domain/__tests__/useAgentChatController.crossChat.test.tsx(existing,upsertMessageswrite witness added)ui/src/hooks/__tests__/useAppController.hostHoldCatalogCache.test.tsxui/src/hooks/__tests__/useAppController.heldHosts.test.tsxChatPage, injected 403 / Chat clickselectableAgentRefs,heldHostsandkeepFocusedChatui/src/__tests__/App.chatDrawer.test.tsxheldHosts: []givesReceived "[]", R2-M2)forgetClosedChatHost(called once per tab close with the closed tab and the remaining tabs)ui/src/__tests__/App.activationSeam.test.tsxui/src/hooks/__tests__/useAppController.heldHosts.test.tsxb8b1232d5)Could not verify access to <name>.then<name> is no longer available., and another Host is selected)ChatPage, injected 4032a150797e:expected null to be 'agent-b')host_access_revoked: toasts exactly the uncertain toast thenAccess to <name> was revoked.)2a150797e)host_access_deniedandhost_access_revoked: a Host neither selected nor on the chat tab gives no toast)blockHostAccesstoasts once per Host per mountCould not verify access to <name>.then<name> is no longer available., no Retry row; returning to Chat selects B once and adds no toast)ChatPage29416e8d1: the loss toast is missing)Access to <name> was revoked.;focusedChatAgentRefstays null)e0e470707)29416e8d1)0e248b3c5:Could not verify access to agent-a.)11146a14e)11146a14e)Team Two Host, and the name that pins the scope clear and the principal gate is asserted inside the switch window, before team 2's catalog landsme.teamId''and'team-1': A held before the team directory arrives after paint stays held, and a failed Retry saysCould not verify access to Research Host.)'': red (base0720afb48);'team-1': green (contract, the directory team equals the session team)me.teamId' team-1 ': the Retry label is the display name)c96aeff2e)host_access_revoked: toasts exactly the uncertain toast, the loss toast andAccess to Research Host was revoked.)ChatPagedb6094475)acac80bcf:expected [ Array(1) ] to deeply equal [])acac80bcf:expected null to be 'agent-b')[null],keepFocusedChatis true, toasts exactly the uncertain toast)ui/src/hooks/__tests__/useAppController.chatDrawer.test.tsxChatPage7fae3f6e9:expected [ null, 'agent-b' ] to deeply equal [ null ])7fae3f6e9)listedterm is dropped from the keep rule7fae3f6e9)ui/src/hooks/domain/__tests__/chatListPerHostPublication.test.tsx!localKeys.has(...)guard is removed, and red when the carry publishes local rows onlyCould not verify access to <name>.)heldAgentRefsignores the catalog of a failed query (R2-L2)ChatPagef92106d69)6b25f9a1f)ui/src/components/__tests__/SidebarNav.test.tsx6b25f9a1f)ui/src/pages/__tests__/ChatPage.autoSelect.test.tsxkeepFocusedChat)ui/src/hooks/__tests__/useAppController.teamSwitchRemoteCarry.test.tsxf92106d69)ui/src/hooks/domain/__tests__/chatListPerHostPublication.test.tsxcarryableAgentscheck is removedChatPagenor the Chat nav pick selects a team-1 Host; once team 2's catalog lands, a team-2 Host is selected)ui/src/hooks/__tests__/useAppController.heldHosts.test.tsxChatPageb8ee26894)b8ee26894)c2584cc27)userId/teamIdequality in the loss check, and red when the loss toast is removedCould not verify access totoast of any Host, then a positive phase)verifyHostAccessis removed (M09)ui/src/hooks/__tests__/useAppController.chatDrawer.test.tsxChatPagef458ac18f)64b56a5b6)523f328e0:expected 'agent-b' to be null)961901294)2692891d5:expected 'agent-c' to be null)2692891d5)9997d58b4)ui/src/hooks/__tests__/useAppController.heldHostRetry.test.tsxChatPageandSidebarNav043470733: active tabchatIdnull)ui/src/hooks/__tests__/useAppController.notificationDrawer.test.tsxChatPage4a1a82dda)Every negative assertion has a liveness witness in the same test, and every test has a vacuity mutation that disables the whole piece.
Mutation table, re-measured on
d2e4cc57c(R5-L5). The earlier per-round tables listed only the test each mutation was written for, so they under-listed the red set. Every mutation of those tables, plus those of the later rounds, was applied alone tod2e4cc57cand run against the 22 UI test files of the held-Host and drawer work (482 tests; baseline 482 passed,EXIT=0). After each run the sources were restored and checked by sha256 (46 of 46); the tree was clean and HEAD unchanged at the end. "Red tests" is the failed count and "Red" the complete list from the JSON reporter: the test id where the title has one, else the quoted title;(×2)marks both cases of anit.each. 45 of 46 mutations turn at least one test red; the remaining one is explained below.keepFocusedChatalso true for a kept Host absent from the catalogkeepFocusedChatalways falsekeepFocusedChat= "the kept Host is held", ignoring the catalogkeepFocusedChatheldAgentRefsinstead of the previous catalogleftCatalogwithout the user/team equalityheldAgentRefsfrom the raw cached catalogselectableAgentRefsfrom the raw cached catalogheldAgentRefsempty while the catalog query has an errorme.teamIdnot trimmedforgetClosedChatHostheldHostswithout the kept chat'schatIdchatId!localKeys.has(...)guard on carried rowscarryableAgentscheckheldHosts: []Two notes on the table:
verifyHostAccessis created once at mount, so without the display-name map in its dependencies the mutated read sees the empty mount-time map and changes nothing. With the map in the dependencies, which is how the defect would actually be written, T-R4L2-3 turns red; the row shows that run.44531cd74removed the tests of the deferred loss toast and its store API (T-A9-P1-1..3, T-A9-T2, T-A9-F1/F1b, T-R1M1-1/2/S,holdKind,heldSince), because that code is gone;2a150797erestored T-A9-P1-4 with the current toast.The T-925 rows are existing tests, green on the base by design; their mutations remove every tombstone defense behind the tested behaviour at once. The late-ack test (T-925-M3) first stayed green under its mutation: it counted only
appendMessages, while sends persist throughupsertMessagessincea7d2e5da8. It now counts both, and the mutation turns it red (expected 2 to be 1).Two coverage gaps in H11, not closed here: no test drives a successful older page followed by a failed probe in the same click, and T-H11-2 does not assert that the server continuation is skipped (its fixture has no server turn markers, so that branch is unreachable before and after the change).
Desktop E2E (real app)
Each fix was also run in the real Electron app against the branch's Minikube profile, with Playwright video, trace and screenshots. The specs and evidence stay outside the repository. Each spec runs on two builds: the fix build and a base build of
57a86a470, thedevcommit before these fixes. The two renderer bundles differ (ui-dist/index.htmlsha256);dist/main.jsis the same, because the PR changes only the renderer.Method.
retries: 0, and no skip or soft assertion.host_missing) places the hold. The CR is restored and read back after every test.aria-current2 ms after the clickchatllm-stateless is no longer available.once, and another Host is selectedCould not verify access to chatllm-stateless., thenchatllm-stateless is no longer available.when the Host leaves the catalog; another listed Host is selected and the removed Host is never selected againShow moreshown againCould not load older messages.Fix runs. All 10 specs passed in one run on
d2e4cc57c(expected 1, unexpected 0, skipped 0, flaky 0each, every specEXIT=0), with the fix build rebuilt on Node 24.22.1. They passed earlier onf92106d69,6b25f9a1f,2a150797e,29416e8d1,0720afb48andb8ee26894. No spec changed after round 5. After the run both removed Hosts were back with their saved spec and a Ready replica.Base notes.
aria-currentis cleared.devca5f0c91fandbffb8f372. The first changes mcp-host and HCC; the second changes Desktop only in the formatting of one type annotation (useAgentChatController.ts:3651) and in tests. Neither touches the code that separates the two builds.44531cd74(before: the deferred toast, and both A2 checks expected that no other Host is selected). F3b-A2 and F3b-A3 changed again with2d9fed46b: another listed Host is selected at the end, and the alerts naming the Host are the uncertain toast then the loss toast. All of these run after A1, the predicted base assertion, so the base results stand.Found by this work and fixed here. F3a and F3b found three gaps:
44531cd74);<name> is no longer available.,87212376f);87212376f, narrowed to held Hosts in44531cd74and to held Hosts that still have a Retry row in2d9fed46b).Not covered.
ce9e43790and6e947f319are proven at Vitest level only.e0e470707,5fd232e07,7346d8bf4,0720afb48) are proven at Vitest level only. In every spec the Host that leaves the catalog is the focused one, and those specs passed unchanged.me.teamIddiffers from its directory team, so the round-5 changes and the later drawer, team-switch, Retry and notification changes (c96aeff2etoe8615977e) are proven at Vitest level only.Found outside this PR.
spec.enabled, so the two control-api checks that read it can never fire (rpcHostAccessAuthorizer.ts:73,accessReconciliation.ts:53).ComposerPanel.tsx:347-350.Validation
On
8df66b51d, with Node 24:npm ciindesktop-app/and in the repository root,npm run verify:electronandnpm run buildpass.48fcd0b30; the difference includes tests added upstream in perf(desktop): reduce chat-open latency and keep reopened transcripts in memory #1112).SidebarNav.test.tsxpasses 29/29 in normal order and with shuffled seeds 1–5.On
0a5285724: the full Desktop suite passes 375 files and 4036 tests, with 0 failed and 0 pending. UI typecheck, Prettier and the staged style rules pass. Removing the newhideAllRemoteRows()call fromresetChatturns T-R1H1-1 red. The rest of R1-H1 does not reproduce: the scope strip runs on the scope change itself and does not wait forloadMenuData. WithloadMenuDatafalse, the list is already empty. The catalog merge rebuilds remote rows from the new page, and T-R1H1-2 covers the deferred-load case.On
046e2894e(merge ofdev81c60e35a), with Node 24, on the exact tree of the commit:npm run verify:electron,npm run build,tsc --noEmit(main andui/), Prettier on the 28 files changed againstdevand the style rules (0 errors) pass. Full Desktop suite: 377 files, 4076 tests passed, 0 failed, 0 pending. The six vacuity mutations still in scope (the guard inChatPage, the four App wiring mutations, T-1130-1) each turn their tests red.On
b8b1232d5, with Node 24:npm run verify:electron,tsc --noEmit(main andui/), Prettier and the style rules pass. Full Desktop suite: 377 files, 4076 tests passed, 0 failed, 0 pending. The new assertion sits inside an existing test, so the count does not change. Mutation: without the Host/id tie-break inbyUpdatedDesc, the test fails withexpected ['stateless-host','stateful-host'] to deeply equal ['stateful-host','stateless-host']; reverted.Minikube T2 on
b8b1232d5(make minikube-t2, thenmake minikube-t2-runtime):scripts/e2e/e2e-wrc-hcc-contracts.shwithpremature completion rejection changed persisted workflow status unexpectedly(T2_EXIT=2). The script postsrunningright after applying the recipe. The WRC reconciler's first pass creates the output anchor pod and writesinitializingwhile the anchor is not Running (workflow-recipes/src/workflow/workflowReconciler.ts:2694-2715). When that write lands after the API's, it reverts the status. Neither file is in this PR's diff, and the same check passed onfab73b054. Tracked in test(e2e): WRC-HCC contract check races the reconciler's first pass on the output anchor pod #1145.make minikube-t2-runtimeon the T0/T1 attestation of run 1:MINIKUBE_T2_PASS,T2_EXIT=0. The check that failed in run 1 passes.#956 (the known
switchToChatflake) was considered for this PR and left out because it did not reproduce: 25 of 25 runs green with every core saturated. Details in #956.On
87212376f: 7 of the new A9 tests fail on the production code ofb8b1232d5and pass with the fix. T-A9-T1 and T-A9-P1-4 pass on both by design. Nine vacuity mutations were run: removing the deferred toast, always using the Retry text, removing the deselect-effect toast, dropping the same-team condition, dropping the scope and epoch guard, dropping only the epoch check, restoring the old ChatPage guard, and makingrefreshreturn nothing. Each turned its tests red. The ninth dropped only the scope check and stayed green, because a scope change also resets the hold store; the check is kept. Full Desktop suite: 377 files, 4085 tests.On
f8a9daff1andad8d013fe: T-A9-F1 fails on87212376f(expected [] to deeply equal [RETRY_A]) and passes after. Reverting the guard turns it red; taking the name from the current catalog turns O1 and O2 red. Full Desktop suite: 377 files, 4087 tests.On
3ab0d8761: T-A9-F1b fails before the change (Received "Research Host is no longer available.") and passes after. Disabling the revoked branch turns only T-A9-F1b red. Full Desktop suite: 377 files, 4088 tests.On
6b25f9a1f, with Node 24:tsc --noEmit(main andui/), Prettier and the style rules (0 errors) pass.ce9e43790and6e947f319fails onf92106d69:expected null to be 'agent-b';expected [ { teamId: 'team-2', … } ] to deeply equal [].expected [ null, 'agent-b' ] to not include 'agent-b');heldSincein the controller turns T-R1M1-1 and T-R1M1-2 red; breakingheldSincein the store turns T-R1M1-S, T-A9-F1 and T-A9-F1b red;invalidateAuthorityScope()call turns T-R1M2-1 red;carryableAgentscheck turns T-R1M3-1 red (expected [ 'local-a', 'remote-a', 'remote-b' ] to deeply equal [ 'local-a', 'remote-a' ]);SidebarNav.test.tsxpasses with shuffled seeds 1–12.Minikube T2 on
f92106d69:MINIKUBE_T2_PASS,T2_EXIT=0(T1: 626 tests, 82 files).Minikube T2 on
6b25f9a1f:MINIKUBE_T2_PASS,T2_EXIT=0(T1: 626 tests, 82 files).The
heldSince,holdKindand deferred-toast tests above were removed in44531cd74together with that code.On
44531cd74, with Node 24:tsc --noEmit(main andui/), Prettier and the style rules pass. Full Desktop suite: 379 files, 4098 tests passed, 0 failed, 0 pending. The four mutations in the Tests section were run and reverted.On
fb1cb2a96:scripts/tests/test-minikube-t2-contract.shandscripts/tests/test-minikube-t1-reporter-report.shpass. Removing the private-suite exclusion from the T1 file list turns the contract test red (T1 selected the private password-recovery suite test/passwordResetPayload.externalRest.realPostgres.integration.test.ts); reverted.On
2a150797e, with Node 24:tsc --noEmit(main andui/) pass. Full Desktop suite: 379 files, 4099 tests passed, 0 failed, 0 pending.Minikube T2 on
2a150797e:make minikube-t2): T0 and T1 passed (T1: 684 tests, 91 files, without the two private suites of fix: restore verified account access under password admission abuse #1056, seefb1cb2a96). The user-facing check NP-08 then failed with exit 137 while HCC rotated the Host token. This is the race tracked in minikube-t2 NP-08: journey exec killed (exit 137) when the fixture patch rolls mcp-host/chatllm #757; neither side of it is in this PR's diff.make minikube-t2-runtimeon the T0/T1 attestation of run 1:MINIKUBE_T2_PASS,T2_EXIT=0.On
2d9fed46btod477e26f8: T-A9-P2 and T-R2L1-1 fail on2a150797eand pass after; the other new tests are green on both by design and red under their mutations (see Tests). Full Desktop suite ond477e26f8: 379 files, 4103 tests.On
29416e8d1, with Node 24.18.0:npm run verify:electron,tsc --noEmit(main andui/) pass.d477e26f8and pass after.Minikube T2 on
29416e8d1(make minikube-t2, planner transitiontargeted-sync): T0, T1 and T2 pass (T1: 684 tests, 91 files),MINIKUBE_T2_PASS,T2_EXIT=0. An earlier run on the same HEAD was stopped in T0 by a SIGTERM from outside the lane (Terminated: 15) and is void. A run ond477e26f8was stopped by hand in T0 when the A22 review led to11b12c476.On
e0e470707to0720afb48, with Node 24:npm run verify:electron,tsc --noEmit(main andui/) pass.0720afb48: 379 files, 4112 tests passed, 0 failed, 0 pending.29416e8d1; T-A23-2 fails one0e470707; T-R4L2-1 fails on0e248b3c5; T-R4L2-2 and T-R4L2-3 fail on11146a14e. Each passes after its fix. T-R4L1-1 is a pin, green before and after.0e248b3c5(test only): before it, about one run in six of the held-Host tests counted the boot's post-paint catalog refresh in the refresh baseline; after it, 5 of 5 runs are green.Minikube T2 on
0720afb48(make minikube-t2on a recreated profile, planner transitionfull-bootstrap): T0, T1 and T2 pass (T1: 684 tests, 91 files),MINIKUBE_T2_PASS,T2_EXIT=0.On
c96aeff2etob8ee26894, with Node 24.22.1:npm run verify:electron,tsc --noEmit(main andui/) pass.b8ee26894: 379 files, 4123 tests passed, 0 failed, 0 pending.'') on0720afb48, T-R5L3-1 onc96aeff2e, T-R5L2-1 ondb6094475, T-R5L4-1 and T-R5L4-2 onacac80bcf, T-R5M1-1, T-R5M1-2 and T-R5M1-4 on7fae3f6e9. T-R5L1-2 and T-R5M1-3 are pins. The full suite on27e3eaa33found one more test that encoded the untrimmed cache scope (1 failed of 4123);d502a829aupdates it.b8ee26894(R5-L5).Minikube T2 on
b8ee26894(make minikube-t2on a recreated profile, planner transitionfull-bootstrap): T0, T1 and T2 pass (T1: 684 tests, 91 files; Desktop suite in the lane: 379 files, 4123 tests),MINIKUBE_T2_PASS,T2_EXIT=0.On
b8ee26894tod2e4cc57c(the merge ofdevincluded), with Node 24.22.1:npm run verify:electronandtsc --noEmit(main andui/) pass ond2e4cc57c.e8615977e: 388 files, 4523 tests passed, 0 failed, 0 pending.d2e4cc57c: 45 of 46 turn at least one test red; the other is explained under the table.Minikube T2 on
d2e4cc57c(make minikube-t2): T0, T1 and T2 pass (T1: 684 tests, 91 files; Desktop suite in the lane: 388 files, 4523 tests),MINIKUBE_T2_PASS,T2_EXIT=0.Review rounds:
62f62def4..9edf68d26was reviewed by GPT-6.1 Sol, GPT-6 Sol, GLM-5.3, Muse Wonder Full and Grok 4.7 Fast. The two findings that needed a change (Retry left the mobile menu open; identical Retry names) are fixed in2033c8616.62f62def4..2033c8616was signed off by Claude Opus 5.5 and Claude Fable 5.1 and audited by GPT-6 Astra. The audit found a test that broke later tests under a shuffled order and a missing focus destination when the narrow-window menu closes. These, and the reviewers' notes on focus after Retry, the session options menu and the visible word "Retry", are fixed inff7899b18.2033c8616..ff7899b18was reviewed by the same three models. Two findings needed a change: the narrow-window backdrop closed the menu without moving focus, and one focus test had no liveness witness of its own. Both are fixed in8df66b51d.ff7899b18..8df66b51dwas signed off by Claude Opus 5.5 and audited by GPT-6 Astra (pass). Both confirm the two fixes above. One note was left as is: see the comment item under "Not in this PR".87212376ffound that a second denial while the loss toast was pending dropped it. Fixed inf8a9daff1;ad8d013feadds the revocation variant found by the second review.3ab0d8761.f92106d69):ce9e43790, R1-M2 in6e947f319. R1-L6 is folded into R1-H1: the ChatPage mock now carrieskeepFocusedChat.6b25f9a1f.44531cd74).44531cd74the hold's toast is shown at once, so no pending toast can meet a failed Retry.d827635de);d477e26f8drops the last two ticket labels (R3-L2).f92106d69..6b25f9a1fand the realism review of the held-Host handling (three independent reviewers) led to44531cd74, which removes the deferred toast,heldSince/holdKind, the per-team listed set and the<name> (<ref>)label.44531cd74), validated by three independent reviewers:2d9fed46b) and R2-L1 (142880262); see F3, "Keeping the chat tab" and its decision table.AgentWorkspace.tsx:286), which do not tell same-name Hosts apart either; a ref suffix only in the Retry row was the<name> (<ref>)branch removed in44531cd74.2d67ff2a7), R2-L5 (555ed4804), R2-L2 in two parts (2a150797e,44013d259).Class-check:/Repro-test:trailers.2a150797e):9c326b615, tightened in29416e8d1.d477e26f8.fb1cb2a96at the top of this body.2a150797e..d477e26f8) was reviewed by GPT-6 Astra (no blocking findings). ASTRA-1 (a held Host behind an open Apps picker was announced as if its tab were focused) and ASTRA-4 (a stale comment) are fixed in11b12c476, ASTRA-2 (T-R3M1-1 did not require the carry itself) in29416e8d1. ASTRA-3 (a positive toast control inside T-F3-2) was not adopted: T-F3-1 and T-R2L1-1 already fail when the toast branch is removed.29416e8d1), validated by three independent reviewers:e0e470707(every held Host that leaves the catalog is announced, whatever has focus) and5fd232e07(a revocation behind the Apps picker is announced). T-A23-1 is rewritten: it pinned the silence and now requires the toast.762bc9607).7346d8bf4, tightened in0720afb48.invalidateAuthorityScopecomment names every continuation it cuts (11146a14e).11146a14e. The reflow ind477e26f8(useAgentChatController.ts:3677-3679) is Prettier formatting only. Round-4 commits carryClass-check:lines with the pattern, the site count and the decision.154442d00stays in this PR.dev(bffb8f372) still hasfs.cpSync(… errorOnExist: true …)atdevGfsStoreFixture.ts:105, andci-public.ymlruns the mcp-host matrix with Node 24 on every PR, so without the commit this PR's CI is red until another PR lands the same fix. The change is test-only and reachesdevwith this PR.devwill be merged here once feat(mcp-host): durable model-step checkpoint and Retry model step (#1043) #1047 lands, andfb1cb2a96then drops out of the diff.29416e8d1..11146a14e) was reviewed by GPT-6 Astra. R4-ASTRA-1 (a team switch could copy the old team's names into the last-known-name map before the new catalog arrived) and R4-ASTRA-3 (a verification pending across a rename used the old name) are fixed in0720afb48. R4-ASTRA-2 (four or more simultaneous losses exceed the 3-toast stack) is listed under "Not in this PR".0720afb48), all six findings reproduced on0720afb48before any change:c96aeff2e(the authority scope uses the trimmed session team), witness follow-up ind502a829a, test tightened inb8ee26894.db6094475(held Hosts are named only from the current principal's catalog).acac80bcf(every hold toast falls back to the last known name).7fae3f6e9(the catalog effect ignores another principal's catalog).eefce7b6e(the drawer stays on the interrupted Host while it is held).27e3eaa33(producer-shaped catalogs in the Host-name tests).6f1313342(the comment gives the reason, not a plan reference).d2e4cc57c, with the complete red list (Tests).Class-check:of7346d8bf4("the others only run for Hosts in the current catalog") did not hold: R5-L3 showed the hold toast and the Retry labels running with a stale catalog during a team switch. Corrected indb6094475, whoseClass-check:counts everyagentDisplayName(call site: 2 inuseAppControllermoved to the resolver, 5 elsewhere kept (chat notifications and chat list titles, not hold paths).11146a14eand0720afb48stay as pushed; splitting them needs a force-push. Each round-5 commit closes one finding.dev(bffb8f372) still has thecpSyncline, and feat(mcp-host): durable model-step checkpoint and Retry model step (#1043) #1047 is still open.0720afb48..d502a829a) was reviewed by GPT-6 Astra. R5-ASTRA-2 (T-R5L1-1 placed the hold after the directory arrived, so its mutation stayed green) is fixed inb8ee26894. R5-ASTRA-1 and R5-ASTRA-3 are fixed inc2584cc27(below).b8ee26894by GPT-6 Sol, GPT-6.1 Sol, GLM-5.3, Grok 4.7 and DeepSeek V4.1 Flash. GPT-6 Sol found two regressions in round 5's own drawer change, both reproduced before any change. R5-ASTRA-1, R5-ASTRA-3 and two of the findings share one cause: the selection and hold readers took the cached catalog without checking its principal.c2584cc27: holds, ChatPage auto-select and the nav-chat pick read only the current principal's catalog (R5-ASTRA-1, R5-ASTRA-3).f458ac18f: a catalog of another principal never becomes the loss baseline (R5-REG-G612).64b56a5b6: the drawer's Host is recorded only when the drawer is open, including width-suppressed (R5-REG-F1).9498bc661: the drawer keeps its Host under the Apps picker (R5-REG-F2).523f328e0mergesdev(ce1b21abf). The dev testApp.activationSeam.test.tsxneeded the new controller field in its mock (fixture only).b8ee26894..9498bc661diff was reviewed by GPT-6 Astra. A29-ASTRA-1 (a conversation held full-screen and restored by the drawer lost its keep) is fixed in6017ac9a1. A29-ASTRA-2 (four no-toast tests had no positive toast phase) is fixed ind57b3fb90;961901294types the new test helpers for the renderertsc.2692891d5.9997d58b4takes the drawer's Host from the last chat selection or drawer request, and removes the marker, its writes in the hold path and the App-owned drawer ref.f550f0fe8;043470733corrects the comments (A33-SOL-P3).043470733by six reviewers who had only the repository, the diff and this PR's stated goal:4a1a82dda: the Retry row carries the kept chat'schatId.8348f9df4.22b1d559d.e8615977e.d2e4cc57cmergesdev(899a76016).Not in this PR
useToastController.ts:17), so the oldest toasts are dropped (R4-ASTRA-2). The cap predates this PR, and no multi-Host removal has been observed.c2584cc27. Pre-existing.loadSessiondenial can hold a Host again after the user has selected another one. Present ondev; no user journey shown.switchTeamForWorkspacewrites an untrimmed team id tocurrentTeamIdReffor one render; the next render writes the trimmed value.loadMessageserror whose text parses as 401 or 403 would hold the Host. The store does not produce such an error.Could not load older messages.with no server fallback; base returned no rows. This is the H11 contract (C-1). The same message appears when server turns rendered but the local cache write failed, for example on a full disk (C-2).closeSidebarMenus(SidebarNav/index.tsx:224-226) says every sidebar navigation closes the menus. New chat and the file tree do not call it; that behaviour predates this branch, and only the comment wording is overstated.🤖 Generated with Claude Code