CURRENT CONTROL-PLANE CHECKPOINT — 2026-10-04 — TERMINAL VIA GATE 4B
ISSUE = #360
CURRENT_MAIN = 9cb74b5c1e16722b4eaee6051ab9b1f6fe0ae56a
STATUS = COMPLETED
FOUNDATION = PR #951
READ_SNAPSHOT_CLOSURE = PR #953
MUTATION_ROOT_RECOVERY_LIFECYCLE = PR #954
PR_954_RESULTING_MAIN = c3afd2b96f20428ae6ebaaa974fd69d1ffc53064
EVIDENCE = docs/native/r15/GATE4B-OPERATION-EVIDENCE.md
EVIDENCE_SCOPE = "This slice is headless Core operation integration for #360"
UNIFIED_CROSS_PROCESS_ADMISSION = IMPLEMENTED
PROTECTED_READ_WRITE_ADMISSION = IMPLEMENTED
LOCK_UNLOCK_SHUTDOWN_INTEGRATION = IMPLEMENTED
RESULTING_MAIN_PROOF = TERMINAL
CURRENT_R15_OWNER = Gate 4D / #359 / QNB-11
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
The historical fs read/write migration-admission race is closed by the terminal renderer-neutral Gate-4B operation integration. No further #360 implementation remains.
HISTORICAL / SUPERSEDED — Successor A exact-head checkpoint — PR #953 — 2026-10-03
PR = #953
HEAD = e9bd7556ab0f8111f843b78038fa56078792a097
BASE = 3cd2b6a37ab8b5bbe8abe32830cd8170b0c9d052
FILES = 15
COMMITS = 3 signed
MEANINGFUL_LINES = 1870
PR_952 = OPEN / DRAFT / frozen provenance
CODEQL = SUCCESS
CHANGELOG/TEXT GUARDS = SUCCESS
CODEANT = all gates SUCCESS
CI_CD = IN_PROGRESS; all Rust/Node/platform/signature/security/build jobs green, E2E/VRT downstream still running
VERCEL = SUCCESS exact head
CODEX_EXACT_HEAD = one new P2 race remains open
MERGE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
The Terra/Codex closure commit e9bd7556... is now on the remote despite the executor's final report having observed a delayed remote ref. The earlier transport blocker is therefore RESOLVED / transient.
The three P2s from 12cf7833... are implemented and their threads are now resolved:
- steady-state validated same-key external forward rebind;
- lifetime-contract cross-process barrier wording;
- admitted snapshot-backed catalog enumeration with raw production list/load paths gated away.
Fresh exact-head Codex review on e9bd7556... found one additional VALID same-slice blocker: a mid-capture TOCTOU can occur when provider.state() observes the old runtime as Unlocked, another process commits root N+1, then the subsequent anchor read sees N+1. Current code publishes N+1 into AuthorityCell.current before proving the provider is bound to N+1; resolve_ref then returns Locked, and future retry cannot take the strict-forward rebind path because N+1 has already poisoned current.
Required invariant for closure: never publish a newer snapshot into current before provider/key usability for that exact anchor is established. Prefer rebinding/validation before publication, or otherwise restore/retain the previous snapshot on resolution failure. Preserve explicit-lock, route-change/rotation, rollback, same-generation mismatch and incompatible-authority refusals.
This is still Read/Snapshot scope, but anti-cascade remains binding. One surgical race fix with a deterministic mid-capture regression is admissible; if it needs broad provider redesign or another architectural expansion, stop/split rather than cascade.
The new CodeScene Large Method warning on the 134-line cross-process integration test is non-material and resolved by evidence; no suppression or proof weakening.
Resource horizon remains unchanged: after #953 protected merge + exact resulting-main CI/CD/CodeQL/Vercel Production + cleanup/retention, executor reports and STOPs. No successor B or 4C.
CURRENT EXECUTION MODE — 2026-10-03 — RESOURCE-BOUNDED GATE 4B SPLIT
CURRENT_MAIN = 3cd2b6a37ab8b5bbe8abe32830cd8170b0c9d052
PR_952 = OPEN / DRAFT / FROZEN PROVENANCE
PR_952_HEAD = e34aae28bba31269a814a9a2778346e568c3577e
PR_952_BUDGET = 16 files / 3000 meaningful lines / 5 signed commits
PR_952_MERGE = FORBIDDEN — validated prepared-root recovery gap remains
GATE_4B_SPLIT = ADMITTED
SUCCESSOR_A = Read/Snapshot Admission Closure
SUCCESSOR_B = Mutation/Root-Recovery/Lifecycle Closure
CURRENT_AGENT_HORIZON = SUCCESSOR_A ONLY → merge → exact resulting-main proof → cleanup/retention → STOP
CONTROL_PLANE_WRITES = ChatGPT session only; coding agent read-only except successor-PR operations
4C = PENDING
4D = PENDING via WorldScript-Studio#359 / QNB-11
4E = PENDING
RELEASE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
Anti-cascade/resource decision: PR #952 is no longer a correction target. It is the immutable reviewed extraction source. The current coding-agent run is deliberately bounded to the first successor PR only and must stop after its normal protected merge, exact resulting-main CI/CD + CodeQL + Vercel Production verification, worktree/branch cleanup, and safe Vercel retention. It must not start successor B or 4C in the same execution epoch.
Control-plane ownership is also split deliberately: GitHub issue, Linear, milestone, release-program and checkpoint curation is performed from the ChatGPT control-plane session; the coding agent should only read those records when needed and should not spend token/week quota duplicating status maintenance.
The prepared-root Step-F coordinator-recovery Critical is Gate 4B successor-B scope, not Gate 4D. Gate 4D remains crash-resumable rotation/rekey after 4B and 4C. #360/QNB-12 remains open until complete Gate 4B closure.
CURRENT R-15 CHECKPOINT — PR 952 / e34aae2 — recovery finding blocks merge
CURRENT_MAIN = 3cd2b6a37ab8b5bbe8abe32830cd8170b0c9d052
4A = TERMINAL via 950
4B_FOUNDATION = TERMINAL via 951
4B_INTEGRATION = ACTIVE / PR 952
PR_952_HEAD = e34aae28bba31269a814a9a2778346e568c3577e
PR_BUDGET = 16 files / 3000 meaningful lines / 5 signed commits
CODEQL = 37132489430 SUCCESS
CI_CD = 37132489420 SUCCESS — all required exact-head jobs terminal green
VERCEL_PREVIEW = dpl_FBpTZftnpnum9ZahLkPCeMfAWUtz READY exact head
CODEX = exact e34aae28 review reports no major issue
CODERABBIT = current incremental review 401b81b8 to e34aae28 complete / no actionable finding
CODEANT_QUALITY_SCR = FAILURE — valid material recovery finding is not dismissed
CODEANT = fresh full review completed; validated prepared-root recovery gap remains OPEN
MERGE_READY = NO
GH_360 = OPEN / QNB-12 In Progress
GATE_4 = ACTIVE / QNB-168 In Progress / M3 Active
4C = PENDING
4D = PENDING via 359 / QNB-11
4E = PENDING
RELEASE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
The minimal stale-CAS correction is signed/pushed and locally verified: success and reconciled stale CAS clear the local mutation latch only after physical/writer/admission revalidation; all uncertain errors remain fail-closed. Normal Git transport works; no sandbox/protection/signing bypass occurred.
New material finding: after an injected step-F COMMIT failure, the public coordinator reconciliation returns PreparationPending even when the provider fault is removed. Existing authenticated recover_root is not integrated into the provider-owning coordinator operation API. Lock/shutdown correctly refuse, but same-instance recovery is incomplete. Diagnostic proof is retained locally; temporary probes were removed from source. Do not merge or close 360/QNB-12 until this is fixed with fault proof and fresh exact-head convergence.
No-op RootBusy is separately proved retryable after contention release; its conservative latch is not the prepared-root recovery defect. Remaining DeepSource Rust / CodeScene reds are understood test-only advisory findings, not green. No 4C journal, 4D rekey, collector, deletion, current-app authority change or Gate-7 switch was introduced. At saturated budget, establish a bounded mutation/root-recovery/lifecycle proof slice rather than expand or delete meaningful safety evidence.
HISTORICAL / SUPERSEDED — prior Gate-4B foundation checkpoint
CURRENT R-15 CHECKPOINT — 2026-10-03 — GATE 4B FOUNDATION TERMINAL VIA #951
CURRENT_MAIN = 3cd2b6a37ab8b5bbe8abe32830cd8170b0c9d052
RESULTING_MAIN_VERIFICATION = TERMINAL
4A = TERMINAL via #950
4B_FOUNDATION = TERMINAL via #951
PR_951_FINAL_HEAD = d3c87fe675300b2d29e797625b192516a94fd191
PR_951_MERGED_AT = 2026-10-03T04:51:40Z
RESULTING_MAIN_CI_CD = 37097936085 SUCCESS
RESULTING_MAIN_CODEQL = 37097936105 SUCCESS
VERCEL_PRODUCTION = dpl_9W7p2XZ4dscci5d2sXasPHK7iibz READY / PROMOTED exact CURRENT_MAIN / HTTP 200
HOUSEKEEPING = COMPLETE — clean local main; merged branch/tracking ref removed; other worktrees preserved
RETENTION = COMPLETE — 3 stale predecessor previews deleted; all 15 protected IDs and alias bindings verified
4B_OVERALL = ACTIVE / #360 / QNB-12
4B_INTEGRATION = NEXT — protected operation / authority-key / snapshot / lifecycle integration
GH_360 = OPEN until complete 4B acceptance
4C = PENDING / authenticated journal + paged manifest
4D = PENDING / #359 / QNB-11 / rekey + key-epoch crash window
4E = PENDING / first enable + disable refusal + Gate 4 closure
GATE_4 = ACTIVE
RELEASE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
All resulting-main CI/CD jobs succeeded, including Security Audit, Verified Signatures, Node 22/24, Core/Tauri Rust, Linux/macOS/Windows platform evidence, Build, Playwright, Deep Coverage, Storybook, Browser Quality, CI Success and Pages. Final-head CodeRabbit/CodeAnt/Codex review converged with no actionable finding and no unresolved thread. The two DeepSource test false positives remain evidence-dispositioned advisory red, not green; unavailable reviewer quotas remain recorded.
This terminal checkpoint is for the kernel admission foundation only. It does not complete issue 360, Gate 4B overall, Gate 4 or production Core authority. The next bounded engineering owner remains 4B integration, not 4C.
HISTORICAL / SUPERSEDED — #951 merged, post-merge proof was pending
CURRENT_MAIN = 3cd2b6a37ab8b5bbe8abe32830cd8170b0c9d052
LAST_VERIFIED_MAIN = 4f33d7786076c1d00e73d5db9c527fbd1ea31693
4A = TERMINAL via #950
4B = ACTIVE / #360 / QNB-12
4B_FOUNDATION_PR = #951 MERGED at 2026-10-03T04:51:40Z
PR_951_FINAL_HEAD = d3c87fe675300b2d29e797625b192516a94fd191
4B_FOUNDATION_TERMINAL = NO — resulting-main + Production + housekeeping pending
RESULTING_MAIN_CI_CD = 37097936085 IN_PROGRESS
RESULTING_MAIN_CODEQL = 37097936105 IN_PROGRESS
VERCEL_PRODUCTION_EXACT_CURRENT_MAIN = PENDING VERIFICATION
GH_360 = OPEN until complete 4B integration acceptance
NEXT_ENGINEERING = 4B protected-operation integration, only after foundation terminal proof
4C = PENDING / journal + paged manifest
4D = PENDING / #359 / QNB-11 / rekey + key-epoch crash window
4E = PENDING / first enable + disable refusal + Gate 4 closure
GATE_4 = ACTIVE
RELEASE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
Final-head CI/CD and CodeQL succeeded; CodeRabbit, CodeAnt and Codex reviewed the final delta/head without remaining actionable findings. All review threads are resolved. The two DeepSource test findings remain evidence-dispositioned advisory false positives, not a green status; quotas are recorded as unavailable. The normal protected squash merge did not close #360 or switch production Core authority. No next semantic slice or retention deletion starts before the exact resulting-main gate.
HISTORICAL / SUPERSEDED — pre-merge #951 checkpoint
CURRENT_MAIN = 4f33d7786076c1d00e73d5db9c527fbd1ea31693
GATE_3 = TERMINAL via #949
GATE_4 = ACTIVE via #922
4A = TERMINAL via #950
PR_950_FINAL_HEAD = f2dde0e65bb4dacfd6753f16a6bce1e28bf13078
RESULTING_MAIN_CI_CD = 36994420903 SUCCESS
RESULTING_MAIN_CODEQL = 36994420828 SUCCESS
VERCEL_PRODUCTION = dpl_7Z1r45ckAp4YeYpdHWtHdzQ8BWNN READY exact CURRENT_MAIN
4B = ACTIVE / #360 / QNB-12 / PR #951 kernel admission foundation; protected-operation integration follows
PR_951_HEAD = d3c87fe675300b2d29e797625b192516a94fd191
PR_951_STATUS = OPEN / exact-head CI and review pending / not terminal
GH_360 = OPEN until full 4B acceptance
4C = PENDING / journal + paged manifest
4D = PENDING / #359 / QNB-11 / rekey + key-epoch crash window
4E = PENDING / first enable + disable refusal + Gate 4 closure
NEXT = 4B → 4C → 4D → 4E → Gate 5 → Gate 6 → explicit Gate 7 authorization
PRE_GATE_7_RESIDUAL = #948
RELEASE_READY = NO
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
Live reconciliation confirms Gate 4A terminal, including exact resulting-main CI/CD, CodeQL and Production evidence. Gate 4B is the current renderer-neutral admission/fencing owner. Historical legacy filesystem defects remain provenance; this checkpoint does not claim a production authority switch or Gate 4 closure.
HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-02 — superseded by verified Gate 4A terminal checkpoint
#360 is no longer blocked on Gate 3. It remains open and is the next semantic closure owner after Gate 4A becomes terminal.
HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-01 — superseded 2026-10-02
OWNER = Gate 4 / #922
PREDECESSOR = Gate 3 terminal
REQUIRED_FIX = coherent read/write/migration admission + cross-process serialization
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO
The historical Tauri race below remains valid provenance, but the implementation destination is now renderer-neutral Gate 4. Do not fix it with a renderer-private lock that the Core production authority could later bypass.
Summary
services/fs/fsCore.ts's protectTextValue()/unprotectTextValue() (and, transitively, every fs-store save/load) resolve the active key and encrypt/decrypt purely via resolveProtectedWriteKey() — they never take the same admission lock the IndexedDB path uses (withProtectedWriteAdmission() / assertNoActiveEncryptionMigration(), see docs/IDB-ENCRYPTION.md). Concretely:
- Writes during a disable/rotate migration.
services/fs/fsEncryptionMigration.ts#migrateAllProtectedFsData re-keys/decrypts every fs-backed file directly, with no lock held against ordinary saves. A normal autosave (project/settings) that resolves its key concurrently with the migration can write a file encrypted under the old key (or plaintext, for disable) after the bridge has already converged that same file, and after clearIdbPassphrase()/rotateIdbPassphrase() swaps or discards the old key — leaving that one file stranded under a key nothing can derive anymore.
- Reads during a migration. Similarly,
unprotectTextValue() doesn't check assertNoActiveEncryptionMigration() before decrypting, so a read racing a migration isn't guaranteed to see a consistent state.
- Legacy plaintext content ignores the lock policy. When encryption is configured but the session is currently locked,
unprotectTextValue() only fails closed for values that parse as a protected-v1 envelope — a value that's still in its pre-migration plaintext form (opportunistic/lazy migration by design, see fsCore.ts's "Protected text files" section) is returned as-is, bypassing the lock gate entirely for any file that hasn't been touched by a save (or the bridge) since encryption was enabled.
Why this wasn't fixed inline
The services/fs/fsEncryptionMigration.ts bridge (built to fix the more severe "disable/rotate strands fs data outright" bug) already substantially narrows the window this issue describes, and per-file read/write failures during migration are now safely skipped (non-strict) or aborted (strict) rather than silently succeeding into a bad state. But it does not prevent the race — it just makes the failure mode of hitting it less catastrophic (a stranded file becomes detectable as "temporarily unreadable" rather than crashing or silently corrupting).
Fixing this properly means giving the fs path the same Web Locks–based admission control the IDB path has (services/storage/storageEncryptionService.ts#withProtectedWriteAdmission), or coordinating both under one shared admission mechanism — a real architectural addition, not a one-line fix, and one best done alongside (or as part of) the resumable-migration work already tracked in #359, since both are about making the fs migration bridge a first-class citizen of the same lifecycle machinery the IDB path already has.
Real fix
protectTextValue() should call the fs equivalent of assertIdbProtectedWriteAllowed() (or take a shared-mode admission) before resolving a key and encrypting.
unprotectTextValue() should call assertSecureStorageReadable() (or equivalent) before returning any content — including a value that isn't (yet) a protected envelope — whenever encryption is configured, so a locked session fails closed uniformly regardless of a given file's migration status.
migrateAllProtectedFsData() should hold the exclusive-mode admission across its whole run, matching how the IDB migration orchestrator already holds it.
Found via
Surfaced during the PR #356 (fix/desktop-project-data-encryption) review-correction loop — codeant-ai, qodo-code-review, and chatgpt-codex-connector independently flagged variants of this same gap (2026-08-13).
Related: #359 (fs migration bridge crash-resumability — same root cause, different symptom).
CURRENT CONTROL-PLANE CHECKPOINT — 2026-10-04 — TERMINAL VIA GATE 4B
The historical fs read/write migration-admission race is closed by the terminal renderer-neutral Gate-4B operation integration. No further #360 implementation remains.
HISTORICAL / SUPERSEDED — Successor A exact-head checkpoint — PR #953 — 2026-10-03
The Terra/Codex closure commit
e9bd7556...is now on the remote despite the executor's final report having observed a delayed remote ref. The earlier transport blocker is therefore RESOLVED / transient.The three P2s from
12cf7833...are implemented and their threads are now resolved:Fresh exact-head Codex review on
e9bd7556...found one additional VALID same-slice blocker: a mid-capture TOCTOU can occur whenprovider.state()observes the old runtime as Unlocked, another process commits root N+1, then the subsequent anchor read sees N+1. Current code publishes N+1 intoAuthorityCell.currentbefore proving the provider is bound to N+1;resolve_refthen returns Locked, and future retry cannot take the strict-forward rebind path because N+1 has already poisonedcurrent.Required invariant for closure: never publish a newer snapshot into
currentbefore provider/key usability for that exact anchor is established. Prefer rebinding/validation before publication, or otherwise restore/retain the previous snapshot on resolution failure. Preserve explicit-lock, route-change/rotation, rollback, same-generation mismatch and incompatible-authority refusals.This is still Read/Snapshot scope, but anti-cascade remains binding. One surgical race fix with a deterministic mid-capture regression is admissible; if it needs broad provider redesign or another architectural expansion, stop/split rather than cascade.
The new CodeScene Large Method warning on the 134-line cross-process integration test is non-material and resolved by evidence; no suppression or proof weakening.
Resource horizon remains unchanged: after #953 protected merge + exact resulting-main CI/CD/CodeQL/Vercel Production + cleanup/retention, executor reports and STOPs. No successor B or 4C.
CURRENT EXECUTION MODE — 2026-10-03 — RESOURCE-BOUNDED GATE 4B SPLIT
Anti-cascade/resource decision: PR #952 is no longer a correction target. It is the immutable reviewed extraction source. The current coding-agent run is deliberately bounded to the first successor PR only and must stop after its normal protected merge, exact resulting-main CI/CD + CodeQL + Vercel Production verification, worktree/branch cleanup, and safe Vercel retention. It must not start successor B or 4C in the same execution epoch.
Control-plane ownership is also split deliberately: GitHub issue, Linear, milestone, release-program and checkpoint curation is performed from the ChatGPT control-plane session; the coding agent should only read those records when needed and should not spend token/week quota duplicating status maintenance.
The prepared-root Step-F coordinator-recovery Critical is Gate 4B successor-B scope, not Gate 4D. Gate 4D remains crash-resumable rotation/rekey after 4B and 4C. #360/QNB-12 remains open until complete Gate 4B closure.
CURRENT R-15 CHECKPOINT — PR 952 / e34aae2 — recovery finding blocks merge
The minimal stale-CAS correction is signed/pushed and locally verified: success and reconciled stale CAS clear the local mutation latch only after physical/writer/admission revalidation; all uncertain errors remain fail-closed. Normal Git transport works; no sandbox/protection/signing bypass occurred.
New material finding: after an injected step-F COMMIT failure, the public coordinator reconciliation returns
PreparationPendingeven when the provider fault is removed. Existing authenticatedrecover_rootis not integrated into the provider-owning coordinator operation API. Lock/shutdown correctly refuse, but same-instance recovery is incomplete. Diagnostic proof is retained locally; temporary probes were removed from source. Do not merge or close 360/QNB-12 until this is fixed with fault proof and fresh exact-head convergence.No-op RootBusy is separately proved retryable after contention release; its conservative latch is not the prepared-root recovery defect. Remaining DeepSource Rust / CodeScene reds are understood test-only advisory findings, not green. No 4C journal, 4D rekey, collector, deletion, current-app authority change or Gate-7 switch was introduced. At saturated budget, establish a bounded mutation/root-recovery/lifecycle proof slice rather than expand or delete meaningful safety evidence.
HISTORICAL / SUPERSEDED — prior Gate-4B foundation checkpoint
CURRENT R-15 CHECKPOINT — 2026-10-03 — GATE 4B FOUNDATION TERMINAL VIA #951
All resulting-main CI/CD jobs succeeded, including Security Audit, Verified Signatures, Node 22/24, Core/Tauri Rust, Linux/macOS/Windows platform evidence, Build, Playwright, Deep Coverage, Storybook, Browser Quality, CI Success and Pages. Final-head CodeRabbit/CodeAnt/Codex review converged with no actionable finding and no unresolved thread. The two DeepSource test false positives remain evidence-dispositioned advisory red, not green; unavailable reviewer quotas remain recorded.
This terminal checkpoint is for the kernel admission foundation only. It does not complete issue 360, Gate 4B overall, Gate 4 or production Core authority. The next bounded engineering owner remains 4B integration, not 4C.
HISTORICAL / SUPERSEDED — #951 merged, post-merge proof was pending
Final-head CI/CD and CodeQL succeeded; CodeRabbit, CodeAnt and Codex reviewed the final delta/head without remaining actionable findings. All review threads are resolved. The two DeepSource test findings remain evidence-dispositioned advisory false positives, not a green status; quotas are recorded as unavailable. The normal protected squash merge did not close #360 or switch production Core authority. No next semantic slice or retention deletion starts before the exact resulting-main gate.
HISTORICAL / SUPERSEDED — pre-merge #951 checkpoint
Live reconciliation confirms Gate 4A terminal, including exact resulting-main CI/CD, CodeQL and Production evidence. Gate 4B is the current renderer-neutral admission/fencing owner. Historical legacy filesystem defects remain provenance; this checkpoint does not claim a production authority switch or Gate 4 closure.
HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-02 — superseded by verified Gate 4A terminal checkpoint
#360 is no longer blocked on Gate 3. It remains open and is the next semantic closure owner after Gate 4A becomes terminal.
HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-01 — superseded 2026-10-02
The historical Tauri race below remains valid provenance, but the implementation destination is now renderer-neutral Gate 4. Do not fix it with a renderer-private lock that the Core production authority could later bypass.
Summary
services/fs/fsCore.ts'sprotectTextValue()/unprotectTextValue()(and, transitively, every fs-store save/load) resolve the active key and encrypt/decrypt purely viaresolveProtectedWriteKey()— they never take the same admission lock the IndexedDB path uses (withProtectedWriteAdmission()/assertNoActiveEncryptionMigration(), seedocs/IDB-ENCRYPTION.md). Concretely:services/fs/fsEncryptionMigration.ts#migrateAllProtectedFsDatare-keys/decrypts every fs-backed file directly, with no lock held against ordinary saves. A normal autosave (project/settings) that resolves its key concurrently with the migration can write a file encrypted under the old key (or plaintext, for disable) after the bridge has already converged that same file, and afterclearIdbPassphrase()/rotateIdbPassphrase()swaps or discards the old key — leaving that one file stranded under a key nothing can derive anymore.unprotectTextValue()doesn't checkassertNoActiveEncryptionMigration()before decrypting, so a read racing a migration isn't guaranteed to see a consistent state.unprotectTextValue()only fails closed for values that parse as aprotected-v1envelope — a value that's still in its pre-migration plaintext form (opportunistic/lazy migration by design, seefsCore.ts's "Protected text files" section) is returned as-is, bypassing the lock gate entirely for any file that hasn't been touched by a save (or the bridge) since encryption was enabled.Why this wasn't fixed inline
The
services/fs/fsEncryptionMigration.tsbridge (built to fix the more severe "disable/rotate strands fs data outright" bug) already substantially narrows the window this issue describes, and per-file read/write failures during migration are now safely skipped (non-strict) or aborted (strict) rather than silently succeeding into a bad state. But it does not prevent the race — it just makes the failure mode of hitting it less catastrophic (a stranded file becomes detectable as "temporarily unreadable" rather than crashing or silently corrupting).Fixing this properly means giving the fs path the same Web Locks–based admission control the IDB path has (
services/storage/storageEncryptionService.ts#withProtectedWriteAdmission), or coordinating both under one shared admission mechanism — a real architectural addition, not a one-line fix, and one best done alongside (or as part of) the resumable-migration work already tracked in #359, since both are about making the fs migration bridge a first-class citizen of the same lifecycle machinery the IDB path already has.Real fix
protectTextValue()should call the fs equivalent ofassertIdbProtectedWriteAllowed()(or take a shared-mode admission) before resolving a key and encrypting.unprotectTextValue()should callassertSecureStorageReadable()(or equivalent) before returning any content — including a value that isn't (yet) a protected envelope — whenever encryption is configured, so a locked session fails closed uniformly regardless of a given file's migration status.migrateAllProtectedFsData()should hold the exclusive-mode admission across its whole run, matching how the IDB migration orchestrator already holds it.Found via
Surfaced during the PR #356 (
fix/desktop-project-data-encryption) review-correction loop — codeant-ai, qodo-code-review, and chatgpt-codex-connector independently flagged variants of this same gap (2026-08-13).Related: #359 (fs migration bridge crash-resumability — same root cause, different symptom).