PacketBenchDocs

Upgrades & migration#

PacketBench has no auto-updater. Every new version is installed by hand, over the top of the old one, and the first launch after that install runs four one-shot migrations before the window appears. Two of those migrations exist because the product was renamed on 2026-08-26, and one of them has a documented consequence that is not going to be fixed.

This page covers upgrading a packaged install, what the two recorded upgrade runs actually proved, the pre-rename migration and its four-way classification of the legacy data directory, and the startup migrations that run whether you asked for them or not.

Before you start#

  • The installer for the version you are moving to, and its SHA-256 from the table in CHANGELOG.md. Every build there is made from a committed tree with a clean working directory, so each artifact maps to exactly one commit.
  • A copy of ~/.packetbench if you have data you care about. The upgrade path has been exercised twice and preserved it both times, which is not the same as a guarantee.
  • On Windows, an existing per-user install. Both recorded upgrades were silent per-user installs; a per-machine or mixed-scope install has never been tried.
  • If the machine carries ~/.packetade from before the rename, read the pre-rename migration section below first. That path is the one still lacking evidence.

There is no auto-updater#

The auto-updater is Planned, not shipped. There is no tauri-plugin-updater dependency in src-tauri/Cargo.toml, no tauri_plugin_updater::init() in src-tauri/src/lib.rs, and no updater block in src-tauri/tauri.conf.json — a search for the string finds nothing in any of the three. dev/updater-setup.md is a runbook for enabling it, and the release gate checks only that the runbook still exists, as a WARN-only row.

Enabling it needs a signing keypair and an HTTPS update server, both deliberately deferred, which makes it an operations task rather than a code change. Diff-patch updates are out of scope on purpose, because the bundle carries a Node sidecar and an externalBin Node runtime that a patch strategy would have to reason about.

The release-readiness script looks for a latest.json update manifest at PACKETBENCH_UPDATER_MANIFEST and three default bundle locations (scripts/release-readiness.mjs:353). It has never found one on a shipped build, so the row reports WARN and the build proceeds. That is the intended behaviour, not a failing check.

Upgrading a packaged install#

  1. Check the installer's SHA-256 against the table for that version in CHANGELOG.md. Do this before running it: the binaries are unsigned, so the hash is the only provenance you have.
  2. Close PacketBench. The installer replaces the executable and the bundled sidecar; a running instance holds both.
  3. Run the installer over the existing install. On Windows both recorded runs were silent per-user:
Start-Process .\PacketBench_0.13.1_x64-setup.exe -ArgumentList '/S','/CURRENTUSER' -Wait
  1. Launch the app. The startup migrations run before the Tauri builder, in the order given under "The one-shot migrations that run at startup" below.

There is no downgrade procedure. Installing an older package over a newer one has never been tried, and the state file is written by whichever build last touched it.

What two upgrade runs proved#

Two same-identifier upgrades have been run, both on 2026-08-30, both silent per-user, both exit 0, both with the installer hash checked first (dev/acceptance.md:104).

Upgrade Date Result
0.12.1 → 0.13.0 2026-08-30 Exit 0; one Add/Remove Programs entry; workspaces and agents unchanged; state.v1.json advanced 69 → 78
0.13.0 → 0.13.1 2026-08-30 Exit 0; one Add/Remove Programs entry; data dir byte-identical — 14 files, 408,164 bytes, state.v1.json v90

The second run is the stronger evidence. The data directory was measured before and after the install and was byte-identical; after launching and using the app it advanced to v99 and 408,200 bytes, so the upgrade preserved state and left it writable. Preserving a directory the app can no longer write to would look identical up to the moment someone tried to use it.

The installed packetbench.exe reports file version 0.13.1. It is not byte-identical to the release/packetbench.exe left in the build tree — the bundler patches that file in place with bundle-type information for each target, so the artifact on disk is not what got packaged. The version resource is the check that means something.

That is the extent of what those runs proved. Sections 2–5 of the acceptance matrix — lifecycle, dictation on real hardware, analytics, and the two-display Monitor matrix — have still never run.

Two limits on what those runs proved#

localStorage does not survive a pre-rename upgrade. WebView2 keys its profile by bundle identifier, and the rename moved it from com.packetade.desktop to com.packetbench.desktop. A package upgraded from a PacketADE build therefore starts against a fresh, empty webview profile, and migrateLegacyStorage() (src/lib/storage-migration.ts:14) finds nothing to migrate. Twelve keys were measured as stranded — pane layouts, dock state, the project-history list and any unsent composer drafts. Nothing is deleted: the old profile stays at %LOCALAPPDATA%\com.packetade.desktop\EBWebView\. This was accepted rather than fixed, because reading another application's storage engine is a feature with its own failure modes, for data that is mostly preference. Upgrades between two PacketBench versions are unaffected, and a source build is unaffected in every case — it keeps the same identifier throughout.

The pre-rename data-dir migration is still unproven on a real installed upgrade. It has only ever been executed from a source build, against a copy of a real legacy data directory. The machine that has been testing this already has ~/.packetbench, so migrate_data_dir_in correctly returns early and the legacy ~/.packetade is left untouched, exactly as designed (src-tauri/src/core/migration.rs:124). Proving the row needs a host or VM snapshot carrying ~/.packetade and no ~/.packetbench.

The keyring migration sits between the two. It has been reviewed line by line — per-key, read-through, write-new-then-delete-legacy, with tests covering partial failure — and no analogous flaw was found. It still needs a packaged run to confirm end to end.

Warning

If your machine carries ~/.packetade data you care about, copy the directory somewhere safe before the first launch of a packaged PacketBench. No packaged installer has ever been installed over pre-rename state, and the migration is the one part of the upgrade path with no installed-upgrade evidence behind it.

Coming from a pre-rename install#

migrate_data_dir() runs before anything else touches the data directory (src-tauri/src/lib.rs:162). It is deliberately cautious, because a sibling packetcode TUI product claimed ~/.packetcode as its own home after the rename shipped, and an earlier PacketCode → PacketADE rename folded some of that product's files into what became PacketBench's directory on real machines.

classify_legacy_dir (src-tauri/src/core/migration.rs:82) decides ownership by the files present, not by the directory name. Eighteen entries are positive evidence of PacketBench — state.v1.json, conversations, pty-transcripts, dictation.db, known_hosts and the rest (migration.rs:17). Seven are positive evidence of the TUI — config.toml, theme.toml, cost-tally.json, computers, jobs, worktrees, workflows (migration.rs:42).

Classification Evidence found What happens
Ours PacketBench markers only Renamed in place to ~/.packetbench — atomic on the same volume.
Mixed Both sets of markers Copy-only. Recognised PacketBench entries are copied out; nothing belonging to the other product is moved, renamed or deleted.
Foreign TUI markers only Left completely alone, with a warning logged.
Unknown Neither Left completely alone. There is nothing of ours to lose, and guessing wrong destroys someone else's data.

sessions/, backups/ and commands/ are deliberately excluded from the PacketBench marker list: the TUI creates directories with those exact names too, so they prove nothing about ownership.

If the same-volume rename fails — a cross-volume home returns ERROR_NOT_SAME_DEVICE on Windows — the migrator falls back to a recursive copy and counts the migration as successful only if the copy fully succeeds. A partial copy is removed, so data_dir() cannot see an incomplete new directory and skip the still-good legacy one. The Mixed rescue never overwrites a file the new directory already has, because merging two live states is not a decision this migration can make (migration.rs:193).

If migration cannot complete, the app keeps reading the legacy directory rather than starting empty: resolve_data_dir falls back to ~/.packetade when, and only when, classify_legacy_dir says it is Ours (src-tauri/src/core/storage.rs:309).

The Mixed case is not hypothetical. A ~/.packetade holding config.toml and cost-tally.json alongside a live state.v1.json at version 1471 — 14 issues and 3 workspaces — was classified Foreign by an earlier build and silently stranded every record. The fix was verified against a copy of that real directory: classified Mixed, all 41,885 bytes recovered byte-identically, the TUI's files left behind, the legacy directory intact.

Keyring entries migrate lazily rather than in a batch. A read falls back to the legacy packetade service, writes the value into packetbench, then deletes the old entry; if the write fails the legacy credential is kept (src-tauri/src/commands/api_keys.rs:38). The same pattern covers the GitHub token (src-tauri/src/commands/github.rs:73) and SSH keys (src-tauri/src/commands/ssh_keys.rs:24).

The one-shot migrations that run at startup#

Four migrations run on every launch and are no-ops after the first one that finds work. Two are in the Rust startup sequence, before the Tauri builder is constructed; two are in the frontend, before any store hydrates.

Order Migration Where Guard
1 core::migration::migrate_data_dir() src-tauri/src/lib.rs:162 ~/.packetbench already exists → return early
2 core::migration::migrate_mission_to_flight() src-tauri/src/lib.rs:166 State file does not contain "missionId" → return early
3 migrateLegacyStorage() src/lib/storage-migration.ts:14 packetbench:migrated-from-packetade key present → return early
4 migrateIssuesMissionToFlight() src/lib/storage-migration.ts:60 packetbench:migrated-mission-to-flight key present → return early

Step 2 canonicalises leftover missionId keys in persisted flight-approval records to flightId. FlightApprovalRequest carries #[serde(alias = "missionId")] (src-tauri/src/core/flight.rs:824), so a single load-and-save round trip through update_state re-serialises the record canonically and drops the legacy key. It is the eager pass that lets the read-side alias be retired a release later, which is why it is guarded on the raw file still containing the string rather than on a version marker.

Step 3 copies packetade:* keys to packetbench:* and leaves the old keys in place as a free rollback path. It never clobbers an existing new key. Step 4 then rewrites the missionId flight link on persisted issues, and must run after step 3 so that packetbench:issues is already in place.

Important

src/main.tsx:1 imports @/lib/run-storage-migration as its first import, and that ordering is load-bearing. ESM hoists static imports, so a bare function call in the body of main.tsx would run after the whole App graph — and every store that reads localStorage at module init — had already evaluated.

A fifth startup pass, core::reprice::reprice_historical_costs() (src-tauri/src/lib.rs:178), is a data rewrite rather than a rename migration: it corrects dollar figures in usage.jsonl and persisted conversation messages that were computed at the old, wrong model rates. It matters on an upgrade because those numbers feed the budget guardrails — a 3× overstated Opus history locks a user out of their own budget. It is idempotent and marks completion with PersistedState.cost_reprice_v1_at.

Adopting memory recorded before remote scoping#

One migration is deliberately not automatic. In earlier builds every memory writer stamped a plain filesystem path regardless of target, so memory recorded on an SSH workspace could never be retrieved from that workspace's remote scope. Those records are still on disk and still unreachable.

When PacketBench finds some, the Memory pane shows an amber banner offering Adopt into <server>. Adoption is opt-in because it is a judgement call: a record stamped /srv/app is indistinguishable from a genuinely local project at /srv/app, and from the same path on a different server. Adopt only if you are sure the memory came from this workspace.

Adoption is reversible. adoptLegacyRemoteMemory stores the record's original path in legacyProjectPath — a field never written by normal capture (src/types/memory.ts:74) — and its presence is what makes Undo exact. revertAdoptedRemoteMemory puts every adopted record back where it was (src/stores/memoryStore.ts:1865). Records that were already correctly scoped are never touched.

Verify#

After the first launch on the new version:

  • Add/Remove Programs holds exactly one PacketBench entry, reading the version you just installed. Two entries mean the bundle identifier changed and you have a parallel install rather than an upgrade.
  • The installed executable reports the new file version. Do not compare it byte-for-byte against the build artifact; the bundler patches that file.
  • ~/.packetbench is still there and still has your workspaces. Check state.v1.json — its version counter advances as you use the app, which is the evidence that the directory is writable and not merely present.
  • ~/.packetade is gone if the migration ran and classified the directory Ours. It is still there, untouched, if the classification was Mixed, Foreign or Unknown — that is correct, not a failed migration.
  • API keys and git-host tokens still work without re-entry. The keyring migration is lazy, so this is confirmed by using a provider, not by looking at a screen.

If it does not work#

What you see What happened
The app starts empty after a pre-rename upgrade The data-dir migration classified ~/.packetade as Foreign or Unknown, or the copy fallback failed. Both cases log a warning and leave both directories intact; nothing is lost.
Pane layouts, dock state and project history are reset Expected on a packaged pre-rename upgrade. The webview profile is keyed by bundle identifier and the old one is stranded, not deleted.
Two PacketBench entries in Add/Remove Programs The two installs have different bundle identifiers. Uninstall the older entry; it does not share a data directory with the new one.
A provider asks for its API key again The lazy keyring migration failed on that key. The legacy credential is kept on a failed write, so re-entering the key is safe.
SmartScreen blocks the installer The binaries are unsigned. Clear it with More info → Run anyway, and only after checking the SHA-256 — see Security & credentials.