pingdotgg / pingdotgg/t3code

connection-catalog.json is written non-atomically every ~3s, and a corrupt document bricks the app permanently

Open
#4,750 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Summary

~/.t3/userdata/connection-catalog.json appears to be written in place, non-atomically, on a fixed ~3 second cadence. If the process dies mid-write (power loss, hard reset, OOM kill), NTFS commits the file's new size but not its data, leaving the file filled entirely with NUL bytes.

On the next launch, desktop.connectionCatalogStore.get throws a decode error and there is no fallback path — the app comes up with an empty project list and a connection that spins on "Connecting…" forever. It never self-heals, because the renderer only calls connectionCatalog.set after a successful get, so no subsequent write ever repairs the file.

The result is a completely unusable app from a single unlucky 3-second window, with no in-app indication of what's wrong.

Impact

After an unclean shutdown, the app launches into this state permanently:

  • Sidebar shows "No projects yet" despite projects existing in state.sqlite
  • The connection (in my case WSL/Ubuntu) is stuck on "Connecting…" indefinitely
  • Agents (Claude, Codex) can't be used, since no connection ever resolves
  • Nothing in the UI surfaces the underlying error — it just looks like data loss

Critically, no data is actually lost. state.sqlite is completely intact. Only a small piece of connection metadata is destroyed, but it takes the entire app down with it.

Reproduction

  1. Have T3 Code open with at least one project and one connection
  2. Kill power to the machine (or hard-kill the process) — the window is ~3s wide, so it lands fairly often
  3. On reboot, check ~/.t3/userdata/connection-catalog.json
  4. Relaunch T3 Code

Expected: app recovers, falls back to a default catalog, rediscovers local/WSL connections Actual: empty project list, connection hangs forever, no error shown

Evidence

The file is 100% NUL bytes
$ xxd ~/.t3/userdata/connection-catalog.json | head -3
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................

size=1408 nul_bytes=1408

Every other JSON file under .t3/ parsed cleanly. This was the only corrupt file on disk.

Timeline from desktop.trace.ndjson
Time (UTC) | Event -- | -- 20:27:18 | last successful desktop.connectionCatalogStore.writeDocument 20:27:22 | file mtime — the write that got truncated by power loss 20:35:35 | first DesktopConnectionCatalogStoreDocumentDecodeError on relaunch every launch since | same error, 16 occurrences per session, zero recovery attempts
The error
DesktopConnectionCatalogStoreDocumentDecodeError: Failed to decode the desktop
connection catalog document at C:\Users\<user>\.t3\userdata\connection-catalog.json.
    at ...\resources\app.asar\apps\desktop\dist-electron\main.cjs:12811:227
    at desktop.connectionCatalogStore.get (...main.cjs:12...)

Caused by: SyntaxError: Unexpected token '\u0000'

It propagates all the way up the IPC chain — every one of these fails together:

desktop.connectionCatalogStore.get   Failure
desktop.ipc.connectionCatalog.get    Failure
desktop.ipc.method                   Failure
desktop.ipc.invoke                   Failure
The write cadence is a constant 3.01s

Gap histogram between consecutive writeDocument spans in a single session:

3.01s → 124    3.02s → 47    3.00s → 8    3.06s → 2    3.05s → 1

That's ~1,200 writes/hour of what is essentially static connection metadata. The spans carry no payload, so I can't confirm the content is unchanged between writes, but a perfectly constant 3.01s interval strongly suggests a polling timer that writes unconditionally rather than on change.

An atomic writer already exists elsewhere in the codebase

I found this sitting next to the corrupt file:

client-settings.json                                       803 bytes  (valid)
client-settings.json.59004.e6b320f650d14d91a69bd7885ebb0da5.tmp   663 bytes  (valid)

That <name>.<pid>.<hash>.tmp pattern is a write-to-temp-then-rename. The settings store was also interrupted by the same power loss — and its real file survived perfectly intact, leaving only an orphaned temp file behind. The connection catalog store, with no such sibling, was destroyed in place.

So the fix pattern is already present in the codebase; the catalog store just isn't using it.

Suggested fixes

In rough priority order:

  1. Write atomically. Reuse whatever the settings store does: write to connection-catalog.json.<pid>.<rand>.tmp, fsync, then rename() over the target. On POSIX and NTFS alike, rename is atomic — a torn write can then only ever leave an orphaned temp file, never a destroyed catalog.
  2. Never let a decode failure be fatal. Treat a malformed document exactly like a missing one: log a warning, move the bad file aside to connection-catalog.json.corrupt-<timestamp>, and fall back to the default catalog so local/WSL connections get rediscovered. A config file that can't be parsed should degrade, not brick.
  3. Only write when the content actually changed. Compare the serialized document to the last-written value and skip the write if identical. This collapses ~1,200 writes/hour to near zero, which shrinks the corruption window by orders of magnitude on its own — and is worth doing regardless of (1) for disk-wear and battery reasons.
  4. Surface the failure in the UI. Right now "No projects yet" + an infinite connection spinner is indistinguishable from real data loss. Even a toast saying "couldn't read connection settings" would have saved a lot of guesswork.

(1) and (2) are independent and both worth having: (1) prevents the corruption, (2) makes the app survive it if it happens anyway through some other route.

Workaround for anyone hitting this now

Quit T3 Code completely (including any lingering backend child process), then:

powershell
mv "$HOME\.t3\userdata\connection-catalog.json" "$HOME\.t3\userdata\connection-catalog.json.bak"

Relaunch. The catalog is regenerated, projects reappear from state.sqlite, and local/WSL connections are rediscovered. Nothing is lost — the corrupt file contained no recoverable bytes anyway. Any manually configured remote connections may need to be re-added.

Environment

  • OS: Windows 11, x64 (NTFS)
  • T3 Code: Alpha channel, t3@0.0.29
  • Backend: WSL backend enabled, distro Ubuntu (wslBackendEnabled: true)
  • Trigger: power surge / unclean shutdown

Possibly unrelated, but noticed while reading the logs

desktop.trace.ndjson records SSH remote pairing tokens in plaintext inside span events:

Connection string: http://127.0.0.1:3774
Token: <REDACTED>
Pairing URL: http://127.0.0.1:3774/pair#token=<REDACTED>

Since these trace files are exactly what users get asked to attach to bug reports, it may be worth redacting tokens at the log sink. Happy to file this separately if you'd prefer.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing desktop.connectionCatalogStore.get and writeDocument, using the connection-catalog.json failure and desktop.trace.ndjson timeline as references. Check the IPC chain through desktop.ipc.connectionCatalog.get and desktop.ipc.invoke, then reproduce the hard-shutdown case. Done means interrupted writes no longer leave an unusable catalog and a corrupt catalog recovers with a usable default or rediscovered connections.

Written by the indexing model from the issue text.

Assessment

Tech stack
electron, typescript
Domain
desktop
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.