Last updated: 2026-07-09
This is the repo-local handoff entrypoint. It exists because future agents should not have to rediscover the project state, document map, or the avatar/SteamVR decisions from chat history.
AGENTS.mdorCLAUDE.md— operating rules, architecture, build commands, safety constraints.docs/NEXT-AGENT-HANDOFF.md— current repo state, last verified build, release checkpoint, and open follow-ups.docs/MD-INDEX.md— all Markdown files and what each one is for.CHANGELOG.md— release history and user-visible behavior.- Only then inspect code.
- Current branch:
main, in sync withorigin/main(0 ahead / 0 behind) as of the v0.15.1 release. HEAD88ef241(release: sync vcpkg.json + complete 0.15.1 changelog). - v0.15.1 is RELEASED to GitHub (tag
v0.15.1+ MSI/ZIP, marked Latest, 2026-07-09). All version files are now in sync at0.15.1(VERSION,web/package.json, README badge/artifacts, andvcpkg.json— bumped from the stale0.14.6at release). Release artifacts + hashes: MSIVRCSM_v0.15.1_x64_Installer.msi(8,970,240 bytes, SHA2565d23965044e3e5136dc968527feecc57b0fc9a69c48dc00370ed9de1825ffd2c), ZIPVRCSM_v0.15.1_x64.zip(19,882,673 bytes, SHA2565d9b152e22298691342d2952c658fb77ac04e522aa1e20ad1cac3ae4ca97951e). TheSHA256:line is present in the published release notes (updater fail-closed gate satisfied). 0.15.1 bundled (in commit order): i18n language-persistence fix, QQ Music lyrics source, OSC progress/marquee sliders, factory-reset thumbnail fix,{music.lyrics}send fix, Kugou 4th lyrics source + QQ title-only fallback (77393a7), gh-pages landing rewrite, and the VRChat-safe OSC seek bar + 4 rich Now Playing presets (600440e). - Lyrics chain is now LRCLIB → NetEase → QQ → Kugou (
web/src/lib/lyrics.ts;LyricsSourceunion includes"kugou"), plus a QQ title-only fallback for obscure/uploaded tracks (title+artist misses them). OSC progress bar is a seek bar━━━━━●────(old▬/▭rendered as empty circles in the VRChat chatbox font); knob optional viaoscProgressBar(...,knob=""). - Working tree is clean apart from one intentionally-untracked scratch file at repo root (
2026-07-04-111708-...txt, a local command transcript). Do NOT commit it. - Release mechanics reminder: bump all of
VERSION,web/package.json, README badge+artifact names together; reconfigure the CMake preset after editingVERSION(it's read at configure time — a plain rebuild says "ninja: no work to do"); the updater's fail-closed hash gate needs theSHA256:line in the GitHub release notes; stop VRCSM before reinstalling or the running WebView2 locksweb/and the MSI silently keeps the stale bundle (a real trap hit this session). - Current test baseline (re-confirmed 2026-07-09 at 0.15.1): C++ ctest 151/151 (3 opt-in live probes DISABLED), web vitest 366/366, Playwright UI smoke 54/54, tsc + release build clean. Run the full web vitest with
--no-file-parallelism— the default parallel runner flakes ~25 fails in two heavy render suites (contention, see [[vitest-parallel-flakiness]]). Only 2 pre-existing compiler warnings remain (PluginBridge.cpp:172u8path C4996,CommonTests.cppgetenv C4996). - Reliability lesson: background workflows/subagents can hang on the inference gateway; prefer foreground single-threaded execution for heavy C++ work. Read-only recon/audit workflows (no heavy compute) ARE reliable and were used heavily this session.
D:\Tool\debugger(Frida/Ghidra/x64dbg/radare2/DIE/FLOSS + MCP bridges) is available for reverse-engineering; Frida 17 API usesProcess.findModuleByName(...).findExportByName(...)(the oldModule.findExportByNameis gone).
A full read-only GUI↔API (IPC) contract audit (docs/review-2026-07/GUI-API-CONTRACT-AUDIT-2026-07-09.md, 185 handlers × 128 call sites, fanned out per bridge domain + adversarially verified, grade B-) was followed by a 9-batch foreground remediation sweep. Each batch was built + tested + committed independently; every behavior change is locked by a new test. Commits (oldest→newest):
1e59f52updater self-replace race —UpdateApplier::Applynow launches a detached cmd bootstrap (wait-for-exit → msiexec in-place → relaunch) instead of racing msiexec against our own shutdown. Real-machine hot-update 0.14.6→0.14.7 verified.b3c01d5LyricsProxy SSRF (CRITICAL) — per-hop redirect refusal (REDIRECT_POLICY_NEVER, 3xx = error), DNS-resolution guard (ResolvesToBlockedAddress), referer CRLF reject. Live NetEase probe still 200 + UTF-8 LRC.ca665fb{error}-as-success —settings.readAll/writeOne/exportReg+config.readrethrow the{error}envelope asIpcException(fixes the Registry-tab white-screen);rethrowIfErrorEnvelopehelper inBridgeCommon.h.835ce0aauth transient-error —auth.status/user,friends.list,moderations.listonly swallowauth_expired; transient (429/500/network) now throws. FEauth-contextpreserves prior authed state on transient errors (kills the logout-flap + cache-wipe loop). Dropped the redundant currentUser probe (N+1).2fafd7dOSC float wire fidelity — tagged-arg wire form{t,v}so whole-number floats keep the,ftag (VRChat drops,ifloats).OscArgumentsFromJsonhonors it; FEcoerceOscValueemits it.0ff7ed4destructive-op guards —junction.repairnow checksProcessGuard::IsVRChatRunning()(its siblings did);vr.audio.switchchecks HRESULTs (was always ok:true). NOTE: the audit's "vr.audio.switch ignores role / hijacks 3 ERoles" framing was WRONG — the 3-ERole loop is correct Windows "set default device" behavior; only the HRESULT-swallow was a real bug.3fd3f9cupdate.download tiering — added toLONG_RUNNING_METHODS(was on the 60s default → spurious timeout on slow MSI download);UpdateDownloadernow requires https.d8c5b0cbatch chunking —images.cache(host caps at 64) andthumbnails.fetch(60s tier) now chunk on the FE so items past the cap aren't silently dropped / the call doesn't time out.6ddef8fanalytics DOT timestamps (HIGH, audit-underrated) —player_events/log_events/world_visitsstoreYYYY.MM.DD HH:MM:SS, butFriendAnalytics::parsePresenceInstantonly parsed ISO → co-presence graph returned empty for ALL real data + activity heatmap grouped everything under NULL. Parser now accepts both shapes; heatmap SQL normalizes dots→dashes inline. Verified against the real db.760bbeberror-code consistency — 7 ApiBridge + 12 PipelineBridgeruntime_error→IpcException{missing_field/not_found};ParamInt64for 9 rowid params (was 32-bit truncation);sqlite3_changes()==0 → not_foundon rules.delete/setEnabled + event.stop/delete;event.addAttendeeINSERT-OR-IGNORE → plain INSERT with UNIQUE(dedupe)/FK(not_found) branch.30e2dcfplugin sandbox 1 —path.probemoved out ofFreeMethods(was zero-permission filesystem-layout leak) behindipc:vrc:cache;screenshots.inject/readMetadatanowensureWithinBasethe screenshots root.c488d66plugin sandbox 2 —auth.user/user.meresults redacted for plugin callers viaPluginRegistry::RedactUserForPlugin(strips steamId/oculusId/email/friends); the SPA still gets the full doc (it bypasses plugin.rpc).
Deliberately NOT changed (documented judgment calls):
fs.listDir/fs.writePlanstay unrestricted for plugin callers — the bundled auto-uploader's core feature is browsing to + uploading from an arbitrary user-chosen folder under an explicitly-granted permission. Scoping them would break it, and they'd need re-plumbing into the plugin-aware handler path.- world_visits mixed-format dwell time (FOLLOW-UP, task open):
world_visitsstores MIXED formats in one column —joined_atis DOT-local, someleft_atrows are ISO with a+09:00offset (7/119).total_hours_in_worldjulianday math over both yields NEGATIVE intervals, so it was left on the prior 0.0 behavior rather than shipping a negative number. Needs offset-aware normalization (likely at the log-parse/ingest layer — investigate why left_at differs from joined_at). Seedocs/review-2026-07/GUI-API-CONTRACT-AUDIT-2026-07-09.md.
Remaining audit batches NOT yet done: B10 (smoke-coverage holes — add /migrate + plugin routes to the matrices, deep-flow tests for avatar delete / 2FA verify / SteamVR repair / data-clear dialogs, shape validators for destructive methods) and B11 (dead-code cleanup of the ~15 confirmed dead handlers/facades). These are lower-severity; the audit report has the full list.
cccb5a6QQ Music lyrics — chain is now LRCLIB → NetEase → QQ (fromQQinweb/src/lib/lyrics.ts: smartbox_new.fcg search → fcg_query_lyric_new.fcg,nobase64=1plaintext LRC, via the hostlyrics.fetchproxy with ay.qq.comReferer;c.y.qq.compasses the SSRF rail). Per-source toggle in NowPlayingPanel; i18n in all 7 locales. Also added an OSC send/listen loopback round-trip test locking the B4 float fix.42ad208i18n language reset — savedvrcsm.languagewas ignored at launch (i18nReady readresolvedLanguagebefore the detector applied the stored value). Now awaits init + reads localStorage directly.d7d484eOSC sliders — progress-bar-width was a bare<Input type=range>(near-invisible in dark theme) → styledSlider; added a marquee-width slider (marqueeWidthwas wired but had no UI control and wasn't passed into the render context).4f60880factory-reset thumbnails — the in-app reset couldn't deletethumb-cache-files/preview-cache/screenshot-thumbs(WebView2 renderer holds the image handles). NowHandlePendingFactoryReset(next-launch, before WebView2 re-inits) wipes them.d1a4d4a{music.lyrics} sent empty — OscTools'musicExtrasomittedmusicLyricLine, so the lyrics card rendered "" even when lyrics were found. Now computes the current line each tick and threads it in.821f3b7opt-in live QQ lyric probe (DISABLED gtest).
Now-playing capture note (verified): the existing GSMTC path already captures QQ Music / Spotify / NetEase / browsers — VRChat need not be running. Live-probe VRCSM_LIVE_NOWPLAYING_TEST reads the real session via ReadNowPlaying(). GSMTC uses GetCurrentSession() (one session); a future improvement is GetSessions() + smart pick when multiple players run.
Open follow-ups (do these next, all lower-risk than they look):
0. Push + release 0.15.1 — DONE (2026-07-09). Committed 88ef241 (vcpkg 0.14.6→0.15.1 + completed changelog), pushed main, tagged + released v0.15.1 with MSI/ZIP (Latest). Full baseline re-verified green first (ctest 151/151, vitest 366/366, Playwright 54/54, tsc + release build clean). Nothing outstanding here.
- Online lyric-source expansion — DONE (
77393a7). Kugou is now the 4th source (chain LRCLIB → NetEase → QQ → Kugou) and QQ has a title-only fallback for obscure/uploaded tracks (e.g. "恶口 1&2 / undaloop" that title+artist missed). Per-source toggle + i18n shipped; tests inlyrics.test.ts. If a track still has no lyrics after this, it's genuinely not in any public source → the local-cache route (#2) or manual-LRC entry are the only options. - Local QQ
.qrccache decrypt — PARKED (hard). QQ caches every played song's lyric at%AppData%\Tencent\QQMusic\QQMusicCache\QQMusicLyricNew\*.qrc(encrypted). Research got the network-qrc algorithm (modified "QQ-DES", keys!@#)(*$%123ZXC!@!@#)(NHL, D-E-D, skip-11, zlib — canonicaldes.cat SuJiKiNen/LyricDecoder / AMLL). BUT: compiling that exact des.c (self-test passes) and decrypting the LOCAL file does NOT yield zlib — the on-disk cache uses a DIFFERENT scheme than the network qrc. Frida-hooked QQMusic: the.qrcopen goes throughstd::_Fiopenin QQMusic.exe and the decode is statically inlined (no exported inflate/uncompress hit); the decoded XML is transient (memory scan found only parsed-object structures). To pursue: locate the inlined decode fn in QQMusic.exe (32-bit, MSVC2017) viaD:\Tool\debugger(Ghidra/x64dbg), or hookReadFileon the .qrc handle + set a hardware bp on the buffer. Only worth it if online sources (#1) don't cover enough — recommend #1 first. - world_visits mixed-timestamp dwell hours (from the GUI-API audit) —
joined_atDOT-local, someleft_atISO+09:00; julianday over both gives negative, left on 0.0. Needs offset-aware normalization at the ingest layer. - Audit batches B10 (smoke coverage) + B11 (dead-code) still open.
- Now-playing music module.
src/core/NowPlaying.{cpp,h}reads the currently-playing system media via Windows GSMTC (C++/WinRTGlobalSystemMediaTransportControls), exposed over themusic.nowPlayingIPC method (src/host/bridges/MusicBridge.cpp). Web consumes it viaweb/src/lib/useNowPlaying.ts;{music.*}OSC tokens (title/artist/album/status/position/duration/progressBar/percent/appName/marquee/lyrics/lyricsTranslated) render throughweb/src/pages/osc/NowPlayingPanel.tsx+ presets. GSMTC async waits are bounded and progress is anchored to sample time. - Synced lyrics.
{music.lyrics}+{music.lyricsTranslated}tokens driven byweb/src/lib/lyrics.tswith a multi-provider chain (LRCLIB exact → LRCLIB search → NetEase) and user-selectable source toggles. Requests route through a NEW C++ host proxysrc/core/LyricsProxy.{cpp,h}via thelyrics.fetchIPC method (src/host/bridges/LyricsBridge.cpp) to bypass WebView2 CORS. The proxy has an SSRF rail (https-only;IsBlockedProxyHostrefuses loopback/link-local/private-range literal hosts — 127/8, 10/8, 192.168/16, 172.16–31, IPv4-mapped IPv6, verifiedLyricsProxy.cpp:108-162). - System tray.
src/host/MainWindow.cppadds a tray icon viaShell_NotifyIconWwith minimize-to-tray and a self-healing NIM_MODIFY→NIM_ADD fallback; maximized-restore fixed. - Robustness/UX. Game Log live-tail backfill of the existing log + precise empty states; FriendLog pagination; clickable notifications; per-subscriber gamelog seed; OSC text-wrap of unbroken strings.
- i18n full parity across all 7 locales (
en,zh-CN,ja,ko,ru,th,hi), 0 placeholder mismatch; non-default locales lazy-loaded. - Database god-object split into a thin
Database.cpp+ 9 domain translation units (Database_Analytics/AssetCache/Avatars/Embeddings/Favorites/Friends/History/Recordings/Rules.cpp) sharingDatabase_internal.h; friend analytics extracted into a pure, testablesrc/core/FriendAnalytics.{cpp,h}.
All three formerly-parked items are done and committed. These shipped as part of v0.15.0 (released 2026-07-09) — the version bump + push + release that was pending at the time is now complete.
- VrcApi transport extraction — DONE (
7112f56). The WinHTTP transport is nowsrc/core/HttpClient.{h,cpp}(vrcsm::core::http):crackUrl,requestOnce/request/get,HttpResponse, rate-limit + 429 retry/backoff, Set-Cookie capture — moved verbatim.VrcApi.cpp(now 3380 lines) keeps all VRChat semantics and delegates through thin wrappers;VrcApi.his byte-frozen (no public API change). Locked by 4HttpClientCrackUrltests + an opt-in live/api/1/configprobe (HttpClientLive, gated onVRCSM_LIVE_VRCAPI_TEST). plugin.marketFeedpermissions — FIXED (133c3af).MarketEntry(PluginFeed.h:59) now has apermissionsvector,ParseFeedreads the entry's optionalpermissionsarray, andMarketEntryToJson(PluginBridge.cpp:78) emits it;docs/gh-pages/plugins.jsoncarries per-entry permissions matching each manifest. The pre-install consent dialog now shows real scopes instead of "none". No TS change. Locked by 3PluginFeedTests.- NetEase Chinese-lyrics — VERIFIED (
45978e5). No production code changed; the shipped path already worked. Added opt-in live gtest probes (LyricsProxyLive.*, DISABLED, gated onVRCSM_LIVE_LYRICS_TEST) that hitmusic.163.comthrough the exactlyrics.fetchtransport — confirmed 200 + raw-UTF-8 Chinese LRC with timestamps. Only the GUI render itself is left (needs a human with a music player + VRChat running).
- session-persist-diagnosis — "每次重登" 真凶是快捷方式指向 7/1 旧 release,持久化层本身正常
- A prior session ran a 6-area multi-agent review; reports are in
docs/review-2026-07/(REVIEW-SUMMARY.mdis the master). It was cut off at 100% context mid-fix. - This session verified/finished all HIGH + security-MEDIUM fixes. Most had already landed; the remaining gaps closed here were lib H2 (LRU
memoSetcap added tothumbnails.ts+assets-cache.ts, matchingimage-cache.ts) and build-docs H1 (.gitignorenow covers_build_*.bat,_tmp_*.bat,*-review.png). - Verified 2026-07-03:
pnpm buildclean,pnpm test238/238,test:smoke27/27, C++ release build up-to-date,ctest100/100 (1 skipped:RealLogClassificationTally). (Superseded by the current baseline in the snapshot above: ctest 128/128, ~347 vitest, UI smoke 54/54.) - See
docs/review-2026-07/REVIEW-SUMMARY.md→ "Remediation Status" for the per-finding evidence table and remaining non-security carry-overs.
- Fixed the async IPC shutdown regression from the bounded-drain optimization:
~IpcBridgenow waits for active queued/running async handlers to finish before destroying tailer/pipeline/DB state. migrate.executeis no longer subject to the frontend's 15-minute response timeout; the UI keeps the pending migration request until the host replies or the session is reset.- MSI packaging again includes
ort-wasm*.wasmbecause experimental avatar visual search loads onnxruntime-web assets from the installedwebtree. - Verified targeted IPC vitest 9/9,
corepack pnpm --dir web build, release host build targetvrcsm, releasectest0 failures out of 104 tests with 5 skipped,package_release.ps1, and MSI decompile showing theort-wasm-simd-threaded.asyncify-*.wasmfile present. (Superseded by the current baseline in the snapshot above: ctest 128/128, ~347 vitest, UI smoke 54/54.)
# 1. Full build
pnpm --prefix web build
cmd.exe /s /c '"D:\Software\Microsoft Visual Studio\18\Community\Common7\Tools\VsDevCmd.bat" -arch=x64 && cmake --build --preset x64-release'
ctest --test-dir build\x64-release --output-on-failure
# 2. Package MSI + ZIP
powershell -NoProfile -ExecutionPolicy Bypass -File .\package_release.ps1
# 3. Upload to GitHub
gh release upload v0.14.6 "build\release\VRCSM_v0.14.6_x64_Installer.msi" "build\release\VRCSM_v0.14.6_x64.zip" --clobberNote: VS2026 path is D:\Software\Microsoft Visual Studio\18 (not D:\Software\MS).
- Avatar rows derived only from VRChat logs do not contain a trustworthy historical thumbnail URL.
- A wearer profile image is the wearer's current public image. It may be useful as a clearly labeled reference, but it must not be shown as a verified historical avatar thumbnail unless the avatar name/current avatar match is verified.
- Successful wearer/reference image lookups are cached by VRCSM, but bulk-loading the entire avatar list made the UI laggy. Keep visible rows fast, lookahead low-priority, and clicked rows highest priority.
- Steam Link / Quest repairs must always be backup-first. Do not delete SteamVR/Steam config directly; move/archive and record what happened.
- Plugin IPC is intentionally permission-scoped. Do not re-expand
ipc:shellinto filesystem access. - Log backfill now runs every startup (not just when DB is empty).
INSERT OR IGNORE+UNIQUEconstraint onworld_visitsprevent duplicates. If data is stale, check that VRChat log directory is being probed correctly. - Non-friend player names are cleaned of VRChat's hex hash suffix by
stripUnresolvedHashSuffix()in both LogParser and LogEventClassifier. vrchat://launchURLs are intercepted in ShellBridge when VRChat is running — uses REST API instead of ShellExecute to avoid spawning a second VRChat.exe.- BoopCard is now emoji-only (no message type tabs, no slot buttons).
- Hardware recommendations (Settings → Hardware) use WMI detection + built-in GPU/CPU score tables.
Use this sequence before claiming a release-facing change is done:
pnpm --prefix web build
pnpm --prefix web test:smoke
cmd.exe /s /c '"D:\Software\Microsoft Visual Studio\18\Community\Common7\Tools\VsDevCmd.bat" -arch=x64 && cmake --build --preset x64-release'
ctest --test-dir build\x64-release --output-on-failure
powershell -NoProfile -ExecutionPolicy Bypass -File .\package_release.ps1If VRCSM.exe is already running, stop it before the C++ release build or the linker may fail with file lock / access denied.
- Do not mutate VRChat user data during tests; use temp directories.
- Destructive operations stay dry-run first.
- Preserve
__infoandvrc-versioninCache-WindowsPlayer. - Keep project docs and release notes aligned with actual shipped behavior.
- Do not update global Codex memory files from inside this repo. This file is the repo-local memory artifact.
- VERSION,
web/package.json, README artifact names, and release asset filenames must stay in sync.