Skip to content

Two state-persistence data-loss bugs: deferred workspace canvas wiped on activation + browser login lost on restart #220

@MaksimMorozovBtx

Description

@MaksimMorozovBtx

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

  1. Have ≥2 projects in the recent list (so at least one is restored as deferred).
  2. Open/activate a non-first project, add 3 terminals to the canvas. Quit (⌘Q).
  3. 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

  1. loadFromProjectFiles hardcodes selectedWorkspaceIndex: 0
    (src/renderer/lib/session.ts). Every recent project except recentProjects[0]
    is restored as a deferred workspace.
  2. 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 storegetCanvasState()
    resolves to the wrong/active canvas, so the placement calls are skipped and
    canvasNodes stays {}. The canvas renders blank.
  3. 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.tssyncCanvasToWorkspace); 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
(installWebContentsSecurityisOAuthUrlshell.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions