Two state-persistence bugs in Cate v1.1.0 (data loss)
App: Cate v1.1.0 · Electron 41.7.1 · Node 24.15.0 · macOS (Darwin 25.3.0)
Both bugs cause silent loss of user state across an app restart. Diagnosed by
extracting app.asar, reading the bundled source maps, and capturing a live
file-watch of .cate/ + ~/Library/Logs/Cate/main.log during a controlled
repro. File/line references below are from the shipped source maps
(dist/main/index.js, src/renderer/...).
Bug 1 — A deferred (non-primary) workspace's canvas is wiped to empty on activation
Symptom
With more than one project in the recent list, opening any project that is not
the first one loses its entire canvas after a restart: terminal/browser panels
still appear in the sidebar tree (e.g. Terminal 4/5/6) but the canvas is blank,
and .cate/workspace.json + .cate/session.json end up with empty nodes.
The first (eagerly restored) project is never affected.
Reproduce
- Have ≥2 projects in the recent list (so at least one is restored as deferred).
- Open/activate a non-first project, add 3 terminals to the canvas. Quit (⌘Q).
- Relaunch. ~10 s after launch (when the deferred workspace is activated), the
canvas goes empty.
What actually happens (captured live)
13:26:22 3 terminals created → workspace.json nodes=3, session.json nodes=3 (good)
13:27:04 ⌘Q (flush save) → main stays 3, .bak ← 3 (quit is fine)
13:27:09 relaunch, "Session restored (3 workspaces)"
13:27:19 ~10s after launch → workspace.json nodes 3 → 0, session.json 3 → 0 (WIPED)
The empty write happens on startup activation, not on quit.
Corroboration across the three recent projects:
SuperPM (index 0, eagerly restored): intact (4 nodes), never lost.
SuperPM/AI_Sec (deferred): wiped to 0.
CateWorkspace (deferred): wiped to 0.
Root cause
loadFromProjectFiles hardcodes selectedWorkspaceIndex: 0
(src/renderer/lib/session.ts). Every recent project except recentProjects[0]
is restored as a deferred workspace.
- When a deferred workspace is activated,
restoreSession recreates the panels
(appStore.createTerminal(...), so they show up in the sidebar tree) but they
are not wired into that workspace's canvas node store — getCanvasState()
resolves to the wrong/active canvas, so the placement calls are skipped and
canvasNodes stays {}. The canvas renders blank.
- The autosave subscription then serializes this state and persists
canvasNodes = {} to disk, overwriting the good file (the previous good copy
only survives in .bak by luck of the prior quit-save).
Three existing safeguards all fail to catch it:
atomicWrite only rejects a zero-byte temp file (dist/main/index.js:3587);
{"nodes":{}} / {"canvas":{"nodes":[]}} are non-empty JSON and pass.
shouldPreserveExistingCanvas (canvasSyncGuard.ts) is only consulted inside
syncCanvasToWorkspace for the selected workspace
(appStore.ts → syncCanvasToWorkspace); the deferred/non-selected save path
bypasses it.
- On load,
isValidWorkspace/isValidSession (index.js:3627/3632) accept an
empty nodes, so tryReadWithFallback returns the empty main file and never
falls back to .bak — the empty state becomes self-perpetuating.
Related papercut
There is no "remove from recent projects" IPC — only recent-projects:get and
recent-projects:add (index.js:1462–1472). "Close Workspace" removes the
workspace from the running session but not from recentProjects, so the
project reappears on next launch and re-enters the buggy deferred path. The only
way to forget a project is to hand-edit config.json.
Suggested fixes
- When activating/restoring a deferred workspace, attach its panels to that
workspace's canvas store before any autosave can fire.
- Apply the
shouldPreserveExistingCanvas guard to all workspaces, not only
the selected one.
- On load, prefer the richer of
main vs .bak (or refuse to overwrite a
non-empty on-disk file with an empty snapshot unless the external-edit guard
cleared it).
- Add a way to remove a project from the recent list.
Bug 2 — Browser panel login is lost on every restart (partition keyed to a regenerated panelId)
Symptom
Log into a site in a browser panel, restart the app → logged out again. Repeats
every restart.
Root cause
The webview uses a persistent partition, but keyed to the runtime panelId:
// src/renderer/panels/BrowserPanel.tsx:369
partition={`persist:browser-${panelId}`}
panelId is regenerated on every session restore: restoreSession's browser
case calls appStore.createBrowser(wsId, url) which mints a new UUID; the
saved panelId from the snapshot is ignored (src/renderer/lib/session.ts,
case 'browser'). So after each restart the panel points at
persist:browser-<new-uuid> — a fresh, empty cookie jar — and the previous
partition is orphaned on disk.
Evidence
~/Library/Application Support/Cate/Partitions/ contains 43 orphaned
browser-<uuid> partitions — one per restart-with-a-browser-panel, each
holding a now-unreachable login.
Suggested fix
Derive the partition from a stable identifier persisted in the snapshot —
either reuse the saved panelId when restoring a browser node, or store a
dedicated stable browserSessionId per node and use
persist:browser-${stableId}.
Secondary note (maybe intentional)
OAuth URLs are force-opened in the external browser
(installWebContentsSecurity → isOAuthUrl → shell.openExternal,
index.js:162598), so even with a stable partition, "Sign in with Google/GitHub"
flows won't complete inside the webview. This looks intentional (security), but
it's worth documenting, since it's the other half of "browser logins don't work".
Diagnosis happy to be extended — full timeline logs and the extracted source
references are available on request.
Two state-persistence bugs in Cate v1.1.0 (data loss)
App: Cate v1.1.0 · Electron 41.7.1 · Node 24.15.0 · macOS (Darwin 25.3.0)
Both bugs cause silent loss of user state across an app restart. Diagnosed by
extracting
app.asar, reading the bundled source maps, and capturing a livefile-watch of
.cate/+~/Library/Logs/Cate/main.logduring a controlledrepro. File/line references below are from the shipped source maps
(
dist/main/index.js,src/renderer/...).Bug 1 — A deferred (non-primary) workspace's canvas is wiped to empty on activation
Symptom
With more than one project in the recent list, opening any project that is not
the first one loses its entire canvas after a restart: terminal/browser panels
still appear in the sidebar tree (e.g.
Terminal 4/5/6) but the canvas is blank,and
.cate/workspace.json+.cate/session.jsonend up with emptynodes.The first (eagerly restored) project is never affected.
Reproduce
canvas goes empty.
What actually happens (captured live)
The empty write happens on startup activation, not on quit.
Corroboration across the three recent projects:
SuperPM(index 0, eagerly restored): intact (4 nodes), never lost.SuperPM/AI_Sec(deferred): wiped to 0.CateWorkspace(deferred): wiped to 0.Root cause
loadFromProjectFileshardcodesselectedWorkspaceIndex: 0(
src/renderer/lib/session.ts). Every recent project exceptrecentProjects[0]is restored as a deferred workspace.
restoreSessionrecreates the panels(
appStore.createTerminal(...), so they show up in the sidebar tree) but theyare not wired into that workspace's canvas node store —
getCanvasState()resolves to the wrong/active canvas, so the placement calls are skipped and
canvasNodesstays{}. The canvas renders blank.canvasNodes = {}to disk, overwriting the good file (the previous good copyonly survives in
.bakby luck of the prior quit-save).Three existing safeguards all fail to catch it:
atomicWriteonly rejects a zero-byte temp file (dist/main/index.js:3587);{"nodes":{}}/{"canvas":{"nodes":[]}}are non-empty JSON and pass.shouldPreserveExistingCanvas(canvasSyncGuard.ts) is only consulted insidesyncCanvasToWorkspacefor the selected workspace(
appStore.ts→syncCanvasToWorkspace); the deferred/non-selected save pathbypasses it.
isValidWorkspace/isValidSession(index.js:3627/3632) accept anempty
nodes, sotryReadWithFallbackreturns the empty main file and neverfalls back to
.bak— the empty state becomes self-perpetuating.Related papercut
There is no "remove from recent projects" IPC — only
recent-projects:getandrecent-projects:add(index.js:1462–1472). "Close Workspace" removes theworkspace from the running session but not from
recentProjects, so theproject reappears on next launch and re-enters the buggy deferred path. The only
way to forget a project is to hand-edit
config.json.Suggested fixes
workspace's canvas store before any autosave can fire.
shouldPreserveExistingCanvasguard to all workspaces, not onlythe selected one.
mainvs.bak(or refuse to overwrite anon-empty on-disk file with an empty snapshot unless the external-edit guard
cleared it).
Bug 2 — Browser panel login is lost on every restart (partition keyed to a regenerated panelId)
Symptom
Log into a site in a browser panel, restart the app → logged out again. Repeats
every restart.
Root cause
The webview uses a persistent partition, but keyed to the runtime
panelId:panelIdis regenerated on every session restore:restoreSession's browsercase calls
appStore.createBrowser(wsId, url)which mints a new UUID; thesaved
panelIdfrom the snapshot is ignored (src/renderer/lib/session.ts,case 'browser'). So after each restart the panel points atpersist:browser-<new-uuid>— a fresh, empty cookie jar — and the previouspartition is orphaned on disk.
Evidence
~/Library/Application Support/Cate/Partitions/contains 43 orphanedbrowser-<uuid>partitions — one per restart-with-a-browser-panel, eachholding a now-unreachable login.
Suggested fix
Derive the partition from a stable identifier persisted in the snapshot —
either reuse the saved
panelIdwhen restoring a browser node, or store adedicated stable
browserSessionIdper node and usepersist:browser-${stableId}.Secondary note (maybe intentional)
OAuth URLs are force-opened in the external browser
(
installWebContentsSecurity→isOAuthUrl→shell.openExternal,index.js:162598), so even with a stable partition, "Sign in with Google/GitHub"flows won't complete inside the webview. This looks intentional (security), but
it's worth documenting, since it's the other half of "browser logins don't work".
Diagnosis happy to be extended — full timeline logs and the extracted source
references are available on request.