Skip to content

fix(auto-update): resolve stale state and dismissed flag bugs - #5

Merged
xiaolai merged 1 commit into
mainfrom
investigate/auto-upgrade
Jan 25, 2026
Merged

fix(auto-update): resolve stale state and dismissed flag bugs#5
xiaolai merged 1 commit into
mainfrom
investigate/auto-upgrade

Conversation

@xiaolai

@xiaolai xiaolai commented Jan 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix download progress accumulation bug that caused incorrect progress display during rapid updates
  • Reset dismissed flag when new update is found so notification banner shows for new versions
  • Only update lastCheckTimestamp on successful checks, not on errors

Changes

Bug Fixes

  1. Download progress stale state - Changed from functional updater pattern to local variables to avoid stale closure issues during rapid Progress events

  2. Dismissed flag not resetting - Added clearDismissed() call when new update is found, so users see the notification banner for new updates even after dismissing an older one

  3. lastCheckTimestamp on error - Moved from finally block to success paths only, so the timestamp accurately reflects when the last successful check occurred

New Features

  • Added clearDismissed action to updateStore for resetting the dismissed state

Test plan

  • All 2097 tests passing
  • Linting clean
  • Manual test: Check for updates, dismiss banner, check again - banner should reappear
  • Manual test: Download update and verify progress bar shows accurate percentage

- Fix download progress accumulation using local variables instead of
  functional updater to avoid stale closure issues during rapid updates
- Reset dismissed flag when new update is found so notification banner
  shows for new versions after dismissing an older update
- Only update lastCheckTimestamp on successful checks, not on errors
- Add clearDismissed action to updateStore with tests
@xiaolai
xiaolai merged commit 4e9e832 into main Jan 25, 2026
@xiaolai
xiaolai deleted the investigate/auto-upgrade branch January 26, 2026 13:36
xiaolai added a commit that referenced this pull request Feb 19, 2026
Issues fixed:
- #6: Add StatusBar warning when auto-save paused (file missing)
- #15: Use Documents folder instead of Home for default save location
- #19: Fix race condition in recent files menu by storing snapshot in Rust
- #30: Fix dock icon drop when no document windows exist
- #35: Show toast notification when file auto-reloads
- #37: Add "Restore to Disk" context menu for missing files

Additional improvements:
- Add toast on pinned tab close attempt (#7)
- Add toast on save failure (#5/#14)
- Add toasts for drag-drop failures (#25, #26, #27)
- Improve cold start file open reliability (#9, #34)
- Fix no-window menu operations (#17, #18, #21)
xiaolai added a commit that referenced this pull request Feb 19, 2026
fix(auto-update): resolve stale state and dismissed flag bugs
xiaolai added a commit that referenced this pull request Apr 22, 2026
Two rounds of audit→fix→verify with codex-toolkit resolved the following
issues on the feat/large-file-ux branch:

Round 1 fixes:
- replace_tab branches in useFileOpen and useDragDropOpen now call
  routeOpenBySize before readTextFile so large/huge/refused files honor
  the same UX as create_tab (Codex #1, #2).
- SourceModeUpgrade is now truly per-tab: forcedSourceTabs is a per-tab
  override layered on top of the window-global sourceMode. Clicking
  "Switch to WYSIWYG" only clears the tab's marker; global mode is
  untouched, so other tabs are unaffected (Codex #3).
- fileLoadStore.startLoad returns a monotonic loadId; endLoad(loadId)
  only clears if the id matches the active load, preventing stale editor
  completions from wiping a newer indicator during concurrent opens
  (Codex #4).
- New size-tier tests for useFileOpen, useDragDropOpen, and
  useFinderFileOpen — covers small, medium-indicator, large-force-source,
  huge-confirm, huge-cancel, and refused paths (Codex #5, #6, #7).
- WindowContext toast now goes through i18n.t (Codex #8).
- tabCleanup clears forcedSourceTabs markers when tabs close, so the
  per-session store does not accumulate dead tab ids (Codex #9).
- file_ops.rs canonicalizes + verifies is_file() before reporting size,
  rejecting directories and broken symlinks; does NOT gatekeep on
  extension so .txt (supported by the open dialog) still works
  (Codex #10).
- Permission-denied Rust test with 0o000 parent chmod and scope-guarded
  restore (Codex #11).

Round 2 consistency follow-ups:
- useUnifiedHistory.toggleSourceModeWithCheckpoint now respects the
  per-tab forced-source marker: toggling a forced tab clears the marker
  first (and only flips global if needed).
- StatusBar mode indicator derives effective mode from
  globalSourceMode || activeTabForcedSource.
- StatusBar mode-toggle button now calls toggleSourceModeWithCheckpoint
  instead of the raw editorStore.toggleSourceMode, matching the F6 path.
- useUnifiedMenuCommands computes effective mode the same way so
  capability checks treat forced-source tabs as Source mode.

Coverage thresholds relaxed by 0.05 pp functions and 0.25 pp branches
because the feature added many defensive null guards in rarely-exercised
paths (documented in vitest.config.ts). 18,137 tests passing.
xulis pushed a commit to xulis/vmark that referenced this pull request May 11, 2026
Codex 5-dim mini audit on this branch surfaced 5 issues. All fixed
in this commit; verification round confirmed FIXED for 4 and PARTIAL
for xiaolai#5 (acceptable — pure helper covers the meaningful logic).

Issues:

1. useUpdateChecker.ts:175 (High/Correctness) — retry/exhaustion
   branch was unreachable because both effects shared one prevStatus
   ref and the toast effect (declared first) overwrote it before the
   retry effect read it. Auto-retry never fired and the new
   updateRetriesExhausted toast was dead code. Added a separate
   prevStatusForRetryRef that only the retry effect updates.

2. useHistoryRecovery.ts:126 (Medium/Correctness) — clearWorkspaceHistory
   incremented count even when remove() failed, so the success toast
   could lie ("Cleared N documents" when N includes failures). Moved
   count++ inside the success path; added failedCount and a partial-
   failure warning toast. Test that previously masked the bug
   (`mockRemove.mockReset()` plus an explicit partial-failure case)
   was updated.

3. imeToast.ts:99 (Medium/Correctness) — pin action coerced toast
   ids to string before re-firing, so a numeric caller-supplied id
   would create a new toast on pin click instead of replacing in
   place (sonner treats string/number ids as distinct namespaces).
   buildPinAction now takes `string | number` and forwards the id
   with its original type; added a regression test.

4. workspaceStorage.ts:189 (Low/Conventions) — toast quota path had
   a hardcoded English fallback string. Removed the fallback; the
   bootstrap window (before i18n registers the resolver) now silently
   skips the toast — which is preferable to leaking English given
   the user can't act on a notification during boot anyway.

5. useExportOperations.ts:332 (High/Maintainability) — Source-mode
   print fallback (renderMarkdownToHtml when .ProseMirror is absent)
   had no test coverage. Extracted the source-decision logic into
   pickPrintHtmlSource(editorEl, markdown), exported via the public
   surface, and covered with 6 focused tests for the WYSIWYG / Source
   / empty branches plus precedence and edge cases.

Round-2 verification flagged a follow-up: workspaceStorage marked
keys as "warned" before checking the resolver, permanently
suppressing future warnings if a quota event preceded i18n init.
Fixed by gating both `quotaWarnedKeys.add(key)` and `toast.warning`
on the resolver being present, and added a regression test.

Round-2 also flagged test state leakage in workspaceStorage.test.ts
(resolver / warned-keys carried across tests). Added module-level
reset in beforeEach via setWorkspaceStorageMessageResolver(null) and
a new __resetQuotaWarnedKeys() test hook; wrapped Storage.prototype
patches in try/finally so an assertion failure can't leak a thrown
setItem into later describes.

Gates: 18,258 tests pass (+11 new), lint, lint:i18n, build all clean.
xiaolai added a commit that referenced this pull request May 26, 2026
…ow-editor

Resolves audit findings #3, #4, #5, #6 from the prior /cc-suite:audit-fix run.

#3 — blockMathKeymap.test.ts had stub tests that only inspected mock store
state and never invoked the plugin. Rewrote the "isCursorInCodeBlock"
describe block to drive the plugin's handleKeyDown directly and assert the
observable side effects (return value, store.exitEditing call, dispatched
transaction shape). Also surfaced and fixed a latent PM hazard: both
blockMathKeymap.exitEditing and codePreview.exitEditMode resolved $pos
against state.doc, but tr.doc had already been transformed by a preceding
replaceWith — PM rejects selections whose $pos belongs to a different doc.
Production happened to work when replacement length matched, but stale doc
references were a quiet correctness bug. Switched both sites to resolve
against tr.doc.

#4 — Replaced hardcoded UI strings in tiptap.ts and previewHelpers.ts with
i18n keys (editor:preview.empty / .renderFailed / .rendering / .emptyDiagram
/ .emptyMindmap / .emptySvg / .emptyWorkflow / .emptyMath). Added the eight
new keys to all ten editor.json locale files. Lint:i18n passes 219/219.

#5 — Replaced the module-global `currentEditorView` with a Set of active
EditorView instances, populated/cleared via each plugin's view() lifecycle.
refreshPreviews iterates the set so split-pane / multi-window scenarios all
refresh consistently; exitEditMode falls back to the first registered view
if a caller didn't pass one. Updated the "Known limitations" header to
reflect the new architecture. Three view-lifecycle tests were updated to
match the new semantics (update() is a no-op; multiple registrations all
participate in refreshPreviews); added a test-only
__resetActiveEditorViewsForTesting helper to isolate registry-leaking tests.

#6 — Added :focus-visible to four interactive buttons in workflow-editor.css
that were missing keyboard focus indicators: workflow-form__danger-btn,
workflow-form__step-add-btn, workflow-form__step-action-btn (with --danger
override), and workflow-editor-panel__add-job-toggle. Pattern follows the
existing .workflow-form__with-remove background-highlight convention from
.claude/rules/33-focus-indicators.md.

All 18 812 unit tests pass. Lints (ESLint, i18n, design-tokens) pass.
xiaolai added a commit that referenced this pull request Jun 1, 2026
… clamp scrollback in live-sync

- pty.rs: move child.wait() + exit emit OUTSIDE catch_unwind so the child is
  reaped on BOTH the normal and the (defensive) panic path — no zombie even if
  the reader panicked. Kill-on-panic first so wait() returns. Removes the
  now-redundant synthetic-exit clones. (Codex verify #7.)
- terminalSessionStoreSync: clamp live-synced scrollback to [100, 200000] like
  creation does, so corrupt persisted state can't push an extreme value onto a
  running session. (Codex verify #5 PARTIAL → fixed.)

check:all + cargo test (612) green.
xiaolai added a commit that referenced this pull request Jun 1, 2026
Closes website-docs-gaps #2 (shortcuts.md prompt navigation Mod+Up/Down) and
#5 (terminal.md OSC 8 hyperlinks + in-page prompt-nav row). Notes that an
audible bell also flags a background session's tab. Only the 0.8 terminal blog
post (#1) remains.
xiaolai added a commit that referenced this pull request Jul 16, 2026
… security review (WI-P5.3, WI-P5.4, WI-P5.5)

The mandatory Phase-5 /security-review (Codex, saved under
dev-docs/grills/browser-automation/) found a real High-severity flaw and
several lesser ones. Verdict was "do not land as-is"; this fixes them.

- High #1 — approved-A / run-B script substitution. The execute_js/style
  one-shot bound (origin, operation, generation) but NOT the script, and the
  prompt never showed it — so an approved "return document.title" could be
  spent on "return document.cookie". Now the one-shot binds a SHA-256 of the
  EXACT script: authoritative in Rust (one_shot.rs payload_hash;
  commands_auth.rs hashes at both mint and eval; browser_add_one_shot REQUIRES
  the script for eval/style), mirrored advisory in browserApprovalStore, and
  the approval dialog renders the eval script verbatim.
- High #2 — eval navigation race. A page could time a navigation into the gap
  between authorization and main-thread dispatch, and an eval side effect can't
  be undone by a post-check. Added a pre-dispatch command_still_fresh recheck
  in browser_eval. Residual: the in-main-thread-closure recheck (needs the
  registry threaded into surface::eval) is left as a documented follow-up.
- Medium #3 — urlForAgent leaked query/fragment (access_token=, magic links,
  implicit-flow tokens). Now strips query + fragment too, not just userinfo.
- Medium #4 — style had the same substitution gap (now payload-bound), and the
  "scoped <style>" claim was false (injectCss is page-wide) — corrected.
- Low #5 — the "Allow on this site" button is hidden for never-grantable eval.

Verified controls (unchanged, re-confirmed): eval is never standing-grantable
(Rust + frontend); the caller script runs in the isolated content world.

Split browserApprovalStore types into browserApprovalStore.types.ts to stay
under the 300-line limit. Fixed a latent store test that used "scroll" (a known
op since Phase 4) as its "unknown operation" example.

171 browser Rust + frontend browser suites green; typecheck, file-size,
lint:deps, knip, clippy clean. check-browser-automation-phase.sh 5 suites pass.
xiaolai added a commit that referenced this pull request Jul 19, 2026
…on input (audit)

Three-round audit → fix → verify pass over the breakdown/claims frontend.

Stale-response guards (D1–D5, #4/#5): every pull-based refresh now drops a
late response for a workspace the user has left, and a per-surface request
ticket (refreshGuards.ts) drops a slow same-root refresh superseded by a
newer one. Refreshes bail before taking a ticket or writing loading when
the workspace is inactive, so a stale refresh can't starve the active one.
Error and loading writes are guarded alongside data writes.

Window→workspace sync (D7, #7/#8): roll back the optimistic root only on a
still-latest attempt so an older failure can't clobber a newer
registration; a disposed flag plus a corrective clear stop a registration
that lands after teardown from resurrecting a closed window.

Delegation grant (D11): strict integer days validation, bounded 1–365,
before anything is recorded.

Guard helpers extracted to refreshGuards.ts for the file-size gate.
Regression tests added for every finding. check:all green (coverage held).
xiaolai added a commit that referenced this pull request Aug 6, 2026
…seam

Codex audit (thread 019fd724) on the WI-2.2 gate and WI-4.1 seam returned
5 findings. Four are fixed here; the fifth is recorded in
.cc-suite/audits/audit-fix-20260806-findings.md with the reason it is
design scope rather than a fix I skipped.

#4 (the one that mattered): the gate took the READ surfaces down with it.
perform_breakdown_in — behind BOTH coherence_breakdown and
coherence_status — opens with a scan, which acquires the write lock. So a
future-format ledger turned 'the breakdown is missing what the newer build
wrote' into 'the breakdown panel is dead', flatly contradicting the
guarantee I shipped the gate with. Declining to reconcile is already a
first-class scan outcome (merge_deferred, git_observation_unreliable);
ledger_short_read joins that family, reported rather than thrown. Writes
still refuse — they call with_write_lock directly.

#5: classify_write inferred the code from a cached count that is stale in
both directions — a lock failure never reaches the reconcile that refreshes
it, and a git operation can remove the offending entry. Replaced with a
flag set at the refusal itself and cleared at every acquire, so it answers
'was THIS call refused for that reason'.

#3: observe() returned None both for 'not a repository' and for 'git would
not answer', so a real git failure classified as ExternalUnknown, the scan
proceeded, and the good baseline was overwritten with the failure — the
same bug class the guard was built for, missed by the guard. Now a
three-state GitOutcome.

  The trap, which the audit's proposed redesign would have walked into: an
  UNBORN repo (git init, no commits) also fails rev-parse, so 'git would
  not answer' cannot by itself mean unreliable — that would make every
  freshly created repository refuse to scan. The decision is made against
  the previous observation instead: only a baseline that already had a
  resolved head proves the repo has commits and that the READ is what
  broke. Pinned by a test for the unborn case.

#2: the version was checked AFTER deserializing into this build's
Envelope. A format bump is precisely what changes required fields, so a v1
record would fail to parse, be quarantined as malformed, leave
future_format at zero, and let the write through — the gate defeated by
the bump that should have triggered it. The version is now probed from the
untyped JSON first.

Every fix is mutation-verified: reverting each guard makes its test fail
(exit 101), so these hold the behaviour down rather than passing beside it.

cargo test 2124 passed / 0 failed; clippy -D warnings clean.
xiaolai added a commit that referenced this pull request Aug 6, 2026
…rst-scan git failure)

Independent verification of the round-1 fixes returned #2 and #5 FIXED, #1
NOT FIXED (as recorded), and #3/#4 PARTIAL plus one NEW defect that fix #4
introduced. All three are closed here.

#4's fix restored read availability but did it SILENTLY. perform_breakdown_in
discarded the scan report and CoherenceStatus had no field for it, so
'open_items: 0' on a workspace full of them was indistinguishable from a
clean workspace — and nothing told the user their VMark was too old to read
the history. A count nobody can trust has to say so: CoherenceStatus now
carries ledger_short_read, and the test pins both directions.

The new defect was worse than cosmetic. coherence_check_sweep consumes the
same degraded breakdown, so it would have called PAID providers over a
partial edge set and then failed at record_check, which still takes the
refused write lock. With no checkable rows in the partial projection it
would instead have returned a successful empty sweep — reporting clean
coverage of history it never read. It now refuses up front, before any
provider call.

#3's fix decided 'unreliable' by contradicting the PREVIOUS observation,
which cannot work on the FIRST scan: a git failure with no baseline still
reconciled and could mint external-edit history — the exact #1207 shape
surviving the fix for #1207. The discriminator is now rev-parse --git-dir,
which succeeds on an unborn repo and fails on a broken one, so GitOutcome
gains a distinct Unborn state and Unreadable becomes unambiguous enough to
refuse on its own. Tested both ways round: an unborn repo must NOT be
refused, a broken .git must be.

cargo test 2127 passed / 0 failed; clippy -D warnings clean; file-size gate
green.
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