- Re-issue post-cleanup quit via setImmediate so Electron doesn't drop it while unwinding the prevented quit
- Use requestAppQuit (not app.quit) on sync window close for forced-exit fallback coverage
- Settle cancelled/timed-out runs immediately when child already exited to avoid hanging on close held open by descendants
- Extract sync-ui-runtime.ts into sync-ui-hosts-state, sync-ui-ipc-handlers, sync-ui-run-lifecycle, sync-ui-snapshots
- Reject sync --ui combined with --remote-cmd or --db in cli-parser-builder
- Replace syncHost/syncSnapshotPath/… with syncCliTokens: string[] passed verbatim to --sync-cli sync
- Move sync CLI validation from launcher to parseSyncCliTokens (app-side single owner)
- Extract resolveImmersionDbPath to shared db-path.ts importable by both launcher and app
- Delete driver.ts; inline types into libsql-driver.ts and hard-wire openLibsqlSyncDb
- Drop OpenSyncDb injection from mergeSnapshotIntoDb and createDbSnapshot
- `runSyncLauncher`: defer exit settlement until result event parsed; handle exit-before-stdout race
- AppImage sync commands now run in Node-only mode (`ELECTRON_RUN_AS_NODE=1 -e ...`) so remote sync needs no display server
- Update docs and changelog to reflect both changes
- Use runAppCommandInteractive so sync-window inherits the terminal directly
- Quit app on window-all-closed when launched with --sync-window on macOS
- Catch synchronous onWillQuitCleanup errors to prevent quit getting stuck
- check-host participates in active-run coordination and can be cancelled
- shutdown() cancels and awaits the active launcher run on quit
- onWillQuit calls preventDefault and re-triggers quit once cleanup settles
- Snapshot mode awaits tracker quiescent before writing output
- Auto-scheduler catches synchronous triggerHostSync failures
- SCP endpoint accepts Windows absolute paths; cmd.exe rejects % in quoted values
- runAppCommand unified (inherit vs pipe stdio) in launcher/mpv.ts
- Docs: add --check, --json, --ui, --sync-cli, --make-temp/--remove-temp examples
The remote tracker increments imm_words/imm_kanji.frequency live, so a
snapshot holding a stale ACTIVE session (skipped by the merge) has that
session's partial occurrences baked into frequency. A word new to the
local DB adopted that full total, then received the same occurrences
again when the session finalized and merged on a later sync.
Subtract active-session occurrence counts when adopting a new row; they
are re-added exactly once when the session completes. Existing rows were
already safe (isNew guard in addWordOccurrences/addKanjiOccurrences).
Verified red/green: the new two-sync regression test fails without the
subtraction (frequency 9 instead of 5).
- reject option-like tokens as flag values (--snapshot --force wrote a
file named --force); --flag=-value still works
- PowerShell remote quoting uses single-quoted literals so $() in a
quoted path cannot expand
- sync-hosts.json written via temp file + rename; a crash mid-write
truncated it and the reader's corrupt-fallback dropped every host
- cancelled sync child escalates SIGTERM -> SIGKILL after 5s grace
- NDJSON progress events validated field-by-field before casting
- snapshot filenames include milliseconds to avoid same-second overwrite
- syncAutoScheduler.stop() wired into will-quit cleanup
- sync --ui exclusivity also rejects --make-temp/--remove-temp/--json
- document --sync-window in app help; group --make-temp/--remove-temp
under modes in sync usage
rollup_day is a local epoch day, but the copy-vs-recompute guard read its
month back via day * 86400 (UTC midnight), which resolves to the previous
civil month for the 1st of a month anywhere west of UTC. Anchor at local
noon (+43200) instead. Pre-existing on main, surfaced by CodeRabbit review.
The engine now executes only inside the app (libsql): subminer sync
rebuilds the equivalent --sync-cli argv and spawns the discovered app
binary with the terminal attached (stdin for ssh prompts, raw NDJSON
stdout, exit-code passthrough). The bun:sqlite driver binding and the
launcher-side engine shims are gone; flow tests moved to
src/core/services/stats-sync/sync-flow.test.ts, ssh tests to src, and
the merge suite now runs through the libsql driver the app ships.
Remote temp dirs are now created and removed by the remote SubMiner
itself (sync --make-temp / --remove-temp, validated against its own
tmpdir) instead of mktemp/rm, and the flow detects the remote shell
(POSIX, cmd, PowerShell) to pick quoting and SubMiner install-location
candidates - %LOCALAPPDATA% app install, launcher shim, or PATH - with
no POSIX PATH prefix on Windows. Remote temp paths are normalized to
forward slashes for scp and the CLI. Verified end-to-end against a
throwaway sshd with the app binary as the resolved remote command.
Sync window drops the launcher-missing warning (self-spawn always works),
the setup checklist explains automatic remote discovery (SubMiner on
PATH, macOS /Applications, or the optional launcher), and the remote
resolver gains tests for app-binary fallback and --remote-cmd probing.
resolveSyncLauncherCommand now returns process.execPath + --sync-cli
(with the app path prepended in dev runs) instead of hunting for bun and
the bundled launcher script. Same NDJSON child protocol, so the sync
window, checks, and auto-sync scheduler are unchanged.
The Electron app now answers launcher-style sync argv (--sync-cli sync
[host|--snapshot|--merge] ... plus --help/--version) at entry, before any
window or display initialization, backed by a libsql binding of the shared
stats-sync engine. Works over SSH with no display server, so a remote
machine no longer needs the bun launcher. The launcher accepts --sync-cli
as a no-op so both invocation shapes stay equivalent.
Move snapshot/merge/quiescence engine and ssh helpers from launcher/sync
into src/core/services/stats-sync, parameterized on a minimal SyncDb
driver. The launcher binds it to bun:sqlite via launcher/sync/bun-driver;
the sync command becomes a thin adapter over the shared sync flow.
- Merge related fix entries into single files (name-match-span-merge into character-name-split-and-scan-recovery, kanji-non-independent-noun-annotations + lexicalized-expression-frequency into content-adverb-annotation-stop-list, greedy-name-parsing-priority into character-name-split-and-scan-recovery, kiku-field-grouping-cancel-double-toast into kiku-field-grouping-modal-recovery, history-rofi-cover-art into launcher-history-command)
- Rewrite remaining entries for brevity and reclassify stats-trend-title-limits as "changed"
- Add allowReadingOnlyMatch:false to kanji token known-word lookups so 渓谷/けいこく no longer matches a mined 警告/けいこく card via reading-only index
- Greedy name pre-pass yields when a strictly longer generic word starts at the same position (空 no longer splits 空気; ties still go to the name)
- 確かに and やはり no longer excluded from frequency/JLPT highlighting and vocab stats
- Stop-word list now covers only interjections, pronouns, and grammar fragments