Skip to content

feat: keep Tcode's data in ~/.tcode on every platform - #601

Merged
Tryanks merged 1 commit into
mainfrom
feat/data-dir-home
Oct 6, 2026
Merged

Tryanks merged 1 commit into
mainfrom
feat/data-dir-home

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Maintainer decisions: the data directory is ~/.tcode on every platform, an older build's directory is moved there at startup with nothing left behind, and legacy/ from the JSONL migration (#594) is removed once the store opens.

Behaviour

  • Resolution has one owner: tcode_services::store::data_dir() → TCODE_DATA_DIR (empty counts as unset), else ~/.tcode; no home directory is an error naming TCODE_DATA_DIR. The traverse native host, the UI's client data dir, headless and the desktop app all use it; traverse's duplicate resolution and its dirs dependency are gone. iOS keeps its sandbox Application Support (its data is a client's pairing files, not a host data dir); Android keeps filesDir.
  • Relocation is part of SessionStore::migrate(), a Relocating phase before the JSONL migration, under the new dir's ownership lock. Source: LEGACY_TCODE_DATA_DIR if set (even with TCODE_DATA_DIR/--data-dir), else the platform directory an older build used (~/Library/Application Support/tcode, $XDG_DATA_HOME/tcode, %APPDATA%\tcode) unless the data dir was chosen explicitly. It runs when the new dir holds no data (tcode.db, sessions.json, settings.json, secrets.json, traverse.json, device.json, *.jsonl; an existing worktrees/ does not count) and the old one does. Every entry is renamed; on the first cross-device error the rest are copied in 8 MiB chunks, verified by length and byte comparison, retired, published, then deleted. A name on both sides stops the move before anything is touched. Cancel is checked per entry and per chunk; the next start resumes from a tcode.relocating/ marker, re-copies a partial file and publishes a verified one. The old dir's lock file and .DS_Store are deleted with the emptied directory.
  • Guards: open_live refuses to open while a move is due (an early open would create tcode.db in the empty new dir and make the move look done); desktop --pair and headless set-password refuse while a move is pending so no pre-move write turns into a collision.
  • legacy/ is removed after every successful open, so users who migrated under feat: keep thread metadata and event logs in a Turso database #594 are cleaned up too; a failed open keeps it.
  • Headless serve runs preparation before --password and the bind check; its banner and phase lines cover the move. Dialog strings no longer claim the originals are kept; both locale files updated. Docs: README, docs/remote.md, CONTRIBUTING ("Verifying real behaviour": LEGACY_TCODE_DATA_DIR).

Evidence

  • Clone of the maintainer's old data dir (9.9 GB, 3,049 entries) with a pre-existing ~/.tcode/worktrees/, scratch paths via TCODE_DATA_DIR/LEGACY_TCODE_DATA_DIR: same-volume rename of all entries in under a second, then the JSONL migration (3,026 threads, 111 s, byte-identical), shell opened, threads render; old dir gone, no legacy/, no marker; 15,028/15,030 non-thread files hash-identical (the other two are settings.json and the model manifest, rewritten by the running app), 26 symlinks identical, worktrees/ untouched.
  • Cross-filesystem (old dir on a disk image): 2 GB synthetic copy verified; SIGKILL mid-copy left a partial file that the restart discarded and re-copied; Ctrl-C mid-copy kept every source; the real profile-homes (919 MB, 947 read-only directories) copied with every permission identical — this run found a bug (read-only directories broke the source deletion), fixed with a regression test.
  • Not observed on screen: the byte-progress line of a copy in the desktop dialog (a 6 GB copy finished in 4 s); the relocating phase was watched through headless output.

Tests

Added: collision stops before anything moves; a cancelled move keeps what it moved and the next start finishes; a copied entry (nested dirs, multi-chunk file, empty file, symlink, read-only tree) arrives verified with permissions preserved and its source deleted; an interrupted copy re-copies a partial file and publishes a verified one; the same directory under another name is not moved; and at the host entry point, a real child process (open_host → needs_migration → migrate → open, as the app does) moves an older data dir in, migrates it and leaves nothing behind, with phases in order.

Changed (contract "originals kept in legacy/" removed by decision): the migration helper asserts legacy/ absent after open, with the database still compared byte for byte against the sources; one test fixture rewrites its sources instead of restoring them from legacy/; the refused-database test now also proves legacy/ survives a failed open; the runtime timeline test drops its legacy/ content assertion (owned by the store tests). No test removed. Not tested by design: "TCODE_DATA_DIR only → no relocation", because a regression would move a developer's real data into a temp dir.

Checks run

cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo nextest run --workspace --locked (911 passed, 11 skipped), cargo machete, Web, iOS simulator and Android (cargo ndk) checks with -D warnings, locale parity, the ignored migration_survives_sigkill. Not run: Windows and Linux (the %APPDATA% → %USERPROFILE%\.tcode path, Windows symlink creation and NTFS read-only removal are checked against std's source only).

  • This change alters the wire protocol, so the next release needs a
    PROTOCOL_VERSION bump: a note was added under "Unreleased" above the
    constant in crates/protocol/src/lib.rs (the number itself changes only
    when the release is cut — CONTRIBUTING.md, principle 9).

The data directory is ~/.tcode on macOS, Windows and Linux, with
TCODE_DATA_DIR still overriding it; one function owns the resolution.
At startup an older build's directory (LEGACY_TCODE_DATA_DIR, else the
platform data directory) is moved in before anything opens the store:
entries are renamed, or copied and verified across filesystems, a name
present on both sides stops the move without touching anything, a
cancelled or interrupted move is continued by the next start, and the
emptied directory is removed. Once the store opens, legacy/ from the
JSONL migration is deleted. The move reports through the same progress
dialog as the migration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant