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
~/.packetbenchif 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
~/.packetadefrom 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#
- 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. - Close PacketBench. The installer replaces the executable and the bundled sidecar; a running instance holds both.
- 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
- 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.
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.
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
PacketBenchentry, 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.
~/.packetbenchis still there and still has your workspaces. Checkstate.v1.json— its version counter advances as you use the app, which is the evidence that the directory is writable and not merely present.~/.packetadeis gone if the migration ran and classified the directoryOurs. It is still there, untouched, if the classification wasMixed,ForeignorUnknown— 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. |
Related#
- Install & first run — prerequisites, the first build, and the platform notes.
- Release status — what has shipped, what is signed, and what remains unproven.
- Where data lives — every file and directory the migrations move.
- Memory — scope keys, and the adoption banner in context.
- Build & release — the release gate, the signing rows, and the updater runbook.
- Troubleshooting — symptoms outside the upgrade path.