Two silent-failure paths, both cheap to close, both load-bearing for the sync work behind them.
1. Identity-schema gating (PRAGMA user_version)
The schema is built entirely from CREATE TABLE IF NOT EXISTS, so an older binary opens a newer database successfully and writes the old model into it. Nothing detects that; the file ends up holding two mutually invisible truths.
Newer than this build → fail closed. There is no safe way to continue.
Older → rebuild the cached message store.
Rebuilding rather than migrating is the pre-release decision in ADR 0002 (PR #385). Message identity cannot be recomputed for rows already stored, so the alternative is keeping them under the old model forever — a store where half the mail is invisible to every guarantee the new model provides.
Preserved
Rebuilt from the server
accounts, credentials, filter rules, WORM audit records
The execution ledger goes because its hash chain is keyed on message ids that would no longer resolve — a chain pointing at absent rows proves nothing, so reinitialising it is more honest than carrying a permanently mixed-namespace log.
2. Scheme-tagged sync checkpoints
The stored checkpoint was "{uidvalidity}:{uidnext}". The QRESYNC checkpoint that replaces it in #390 is "{uidvalidity}:{highestmodseq}" — identical shape, different meaning.
An upgraded engine reading one as the other hands the server a UIDNEXT where it expects a MODSEQ. UIDNEXT is normally the larger number, so the server answers nothing changed since, and every message below that point is never enumerated again. Silent, permanent mail loss with no error anywhere.
Tokens are now "v1:uidnext:{uidvalidity}:{uidnext}", and anything whose scheme this build does not recognise — an unknown tag, a future tag, or an untagged legacy value — resolves to a full re-fetch. Over-fetching is recoverable; misreading is not.
Type of Change
Bug fix (non-breaking change fixing an issue)
New feature (non-breaking change adding functionality)
Breaking change (fix or feature causing existing functionality to change)
I have verified code formatting (cargo fmt --all -- --check)
I have verified clippy lints (cargo clippy --all-targets --workspace -- -D warnings)
I have executed the full test suite (cargo test --workspace) and all tests pass (599 passed, 0 failed)
Business logic is implemented in headless engine crates, keeping UI presentation shells skinny
No secrets, keys, or credentials are added to tracked files
Notes for review
IDENTITY_SCHEMA_VERSION starts at 1 and is deliberately distinct from the additive column migrations elsewhere in db.rs. Those add nullable columns and leave every row meaningful; this counter is only for changes to what a stored message is. Bump it only alongside the reset it implies — #394 will be the first real bump.
Existing checkpoint tests were retagged, and a new test pins the four dangerous inputs (untagged, future scheme, wrong scheme, truncated tag) all resolving to a full re-fetch.
reconcile_schema_version runs after the tables exist and after the additive migrations, so a rebuild lands on the current shape rather than an intermediate one.
No .proto change.
## Summary of Changes
Two silent-failure paths, both cheap to close, both load-bearing for the sync work behind them.
### 1. Identity-schema gating (`PRAGMA user_version`)
The schema is built entirely from `CREATE TABLE IF NOT EXISTS`, so **an older binary opens a newer database successfully** and writes the old model into it. Nothing detects that; the file ends up holding two mutually invisible truths.
- **Newer than this build** → fail closed. There is no safe way to continue.
- **Older** → rebuild the cached message store.
Rebuilding rather than migrating is the pre-release decision in [ADR 0002](docs/adr/0002-multi-engine-sync-model.md#pre-release-posture) (PR #385). Message identity cannot be recomputed for rows already stored, so the alternative is keeping them under the old model forever — a store where half the mail is invisible to every guarantee the new model provides.
| Preserved | Rebuilt from the server |
| --- | --- |
| accounts, credentials, filter rules, WORM audit records | messages + FTS, folder checkpoints, filter execution ledger |
The execution ledger goes because its hash chain is keyed on message ids that would no longer resolve — a chain pointing at absent rows proves nothing, so reinitialising it is more honest than carrying a permanently mixed-namespace log.
### 2. Scheme-tagged sync checkpoints
The stored checkpoint was `"{uidvalidity}:{uidnext}"`. The QRESYNC checkpoint that replaces it in #390 is `"{uidvalidity}:{highestmodseq}"` — **identical shape, different meaning**.
An upgraded engine reading one as the other hands the server a UIDNEXT where it expects a MODSEQ. UIDNEXT is normally the larger number, so the server answers *nothing changed since*, and every message below that point is never enumerated again. **Silent, permanent mail loss with no error anywhere.**
Tokens are now `"v1:uidnext:{uidvalidity}:{uidnext}"`, and anything whose scheme this build does not recognise — an unknown tag, a future tag, or an untagged legacy value — resolves to a full re-fetch. Over-fetching is recoverable; misreading is not.
## Type of Change
- [x] Bug fix (non-breaking change fixing an issue)
- [x] New feature (non-breaking change adding functionality)
- [ ] Breaking change (fix or feature causing existing functionality to change)
- [ ] Documentation update
## Related Issues
Refs #387. Prerequisite for #390 and #394.
## Quality Verification Checklist
- [x] I have verified code formatting (`cargo fmt --all -- --check`)
- [x] I have verified clippy lints (`cargo clippy --all-targets --workspace -- -D warnings`)
- [x] I have executed the full test suite (`cargo test --workspace`) and all tests pass (**599 passed, 0 failed**)
- [x] Business logic is implemented in headless engine crates, keeping UI presentation shells skinny
- [x] No secrets, keys, or credentials are added to tracked files
## Notes for review
- **`IDENTITY_SCHEMA_VERSION` starts at 1** and is deliberately distinct from the additive column migrations elsewhere in `db.rs`. Those add nullable columns and leave every row meaningful; this counter is only for changes to *what a stored message is*. Bump it only alongside the reset it implies — #394 will be the first real bump.
- **Existing checkpoint tests were retagged**, and a new test pins the four dangerous inputs (untagged, future scheme, wrong scheme, truncated tag) all resolving to a full re-fetch.
- `reconcile_schema_version` runs after the tables exist and after the additive migrations, so a rebuild lands on the current shape rather than an intermediate one.
- No `.proto` change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary of Changes
Two silent-failure paths, both cheap to close, both load-bearing for the sync work behind them.
1. Identity-schema gating (
PRAGMA user_version)The schema is built entirely from
CREATE TABLE IF NOT EXISTS, so an older binary opens a newer database successfully and writes the old model into it. Nothing detects that; the file ends up holding two mutually invisible truths.Rebuilding rather than migrating is the pre-release decision in ADR 0002 (PR #385). Message identity cannot be recomputed for rows already stored, so the alternative is keeping them under the old model forever — a store where half the mail is invisible to every guarantee the new model provides.
The execution ledger goes because its hash chain is keyed on message ids that would no longer resolve — a chain pointing at absent rows proves nothing, so reinitialising it is more honest than carrying a permanently mixed-namespace log.
2. Scheme-tagged sync checkpoints
The stored checkpoint was
"{uidvalidity}:{uidnext}". The QRESYNC checkpoint that replaces it in #390 is"{uidvalidity}:{highestmodseq}"— identical shape, different meaning.An upgraded engine reading one as the other hands the server a UIDNEXT where it expects a MODSEQ. UIDNEXT is normally the larger number, so the server answers nothing changed since, and every message below that point is never enumerated again. Silent, permanent mail loss with no error anywhere.
Tokens are now
"v1:uidnext:{uidvalidity}:{uidnext}", and anything whose scheme this build does not recognise — an unknown tag, a future tag, or an untagged legacy value — resolves to a full re-fetch. Over-fetching is recoverable; misreading is not.Type of Change
Related Issues
Refs #387. Prerequisite for #390 and #394.
Quality Verification Checklist
cargo fmt --all -- --check)cargo clippy --all-targets --workspace -- -D warnings)cargo test --workspace) and all tests pass (599 passed, 0 failed)Notes for review
IDENTITY_SCHEMA_VERSIONstarts at 1 and is deliberately distinct from the additive column migrations elsewhere indb.rs. Those add nullable columns and leave every row meaningful; this counter is only for changes to what a stored message is. Bump it only alongside the reset it implies — #394 will be the first real bump.reconcile_schema_versionruns after the tables exist and after the additive migrations, so a rebuild lands on the current shape rather than an intermediate one..protochange.