From 9723661c80babc97637319d312eeeb2a3e53f8a7 Mon Sep 17 00:00:00 2001 From: Evgeny Zemtsov Date: Wed, 18 Feb 2026 18:51:17 +0100 Subject: [PATCH] handle recycled server object IDs for workspace/group handles (#1725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When switching tabs rapidly or closing multiple tabs, the taskbar shows "ghost" workspaces — entries with no name, no coordinates, and no active state. The ghosts appear at positions where workspaces were removed and then recreated by the compositor. When a compositor removes a workspace (sends `removed` event) and the client calls Destroy(), the proxy is marked as zombie but stays in the Context.objects map. For server-created objects (IDs >= 0xFF000000), the server never sends `delete_id`, so the zombie proxy persists indefinitely. When the compositor later creates a new workspace that gets a recycled server object ID, GetProxy() returns the old zombie proxy. The dispatch loop in GetDispatch() checks IsZombie() and silently drops ALL events for zombie proxies — including property events (name, id, coordinates, state, capabilities) intended for the new workspace. This causes the ghost workspaces with empty properties in the UI. Fix: check IsZombie() when handling `workspace` and `workspace_group` events that carry a `new_id` argument. If the existing proxy is a zombie, treat it as absent and create a fresh proxy via registerServerProxy(), which replaces the zombie in the map. Subsequent property events are then dispatched to the live proxy. --- core/internal/proto/ext_workspace/workspace.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/internal/proto/ext_workspace/workspace.go b/core/internal/proto/ext_workspace/workspace.go index b35a8853..c76ab982 100644 --- a/core/internal/proto/ext_workspace/workspace.go +++ b/core/internal/proto/ext_workspace/workspace.go @@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) { l := 0 objectID := client.Uint32(data[l : l+4]) proxy := i.Context().GetProxy(objectID) - if proxy != nil { + if proxy != nil && !proxy.IsZombie() { e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1) } else { groupHandle := &ExtWorkspaceGroupHandleV1{} @@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) { l := 0 objectID := client.Uint32(data[l : l+4]) proxy := i.Context().GetProxy(objectID) - if proxy != nil { + if proxy != nil && !proxy.IsZombie() { e.Workspace = proxy.(*ExtWorkspaceHandleV1) } else { wsHandle := &ExtWorkspaceHandleV1{}