Repository navigation
Land the Quest integration on main (#239) - #326
Merged
Merged
Conversation
…#158) Co-Authored-By: Andrew Bates <a@sprock.io>
…refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…claude-quest-hooks' into claude-quest-router * origin/claude-evr-codec: refactor(compat): extract the EVR frame codec into a platform-neutral evr_codec (refs #158) * origin/claude-quest-hooks: test(quest): lifecycle gtests, pinned-ELF resolution, wire quest hooks into verify (refs #158) test(quest): host tests for the GOT backend and callback thunks (refs #158) feat(quest): exact GOT backend, shared hook lifecycle header, typed callback thunks (refs #158)
…k; tighten codec tests (refs #158) - ws_bridge: the game->server diagnostic walk is now LogGameToServerFrame. It stops on a Truncated message before the PlayerSessionRequest hex dump and the invite decoders read the payload, reads the payload through the codec's message.payload, and prints the declared length unwrapped. A declared length of 2^64-24 made the old total wrap to 0, so the loop never advanced. - evr_codec: remove ParseLoginRequest and LoginRequestStatusName (no production caller); the reader lives in tests/evr_codec_test_reader.h. Restore the platform-prefix and server-key bearer rationale as comments. - Tests: call-site tests for the walk (truncated invite, truncated PlayerSessionRequest, wrapping length), a wrapping-length codec case in both test files, and a caught nlohmann::json::exception in the Quest test. - test-quest-shared takes nlohmann/json.hpp from the build's vcpkg_installed tree and fails with a message when it is absent, instead of relying on a system package. - ADR 0003 diagram column aligned. Co-Authored-By: Andrew Bates <a@sprock.io>
…d of dying in the glob (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…quest-login * origin/claude-quest-hooks: test(quest): lifecycle gtests, pinned-ELF resolution, wire quest hooks into verify (refs #158) test(quest): host tests for the GOT backend and callback thunks (refs #158) feat(quest): exact GOT backend, shared hook lifecycle header, typed callback thunks (refs #158)
…ig read, log structured Review findings on the Quest config: - The resolved config lived in a namespace-scope object with a dynamic initializer that ran after the ELF constructor and blanked it. It is now a function-local static; sentinel.cpp's dump directory string gets the same fix. - The config read opens non-blocking and requires a regular file, so a FIFO at the path falls back to the embedded defaults instead of hanging startup. - The disk log is one JSON object per line; open and write failures are each reported once and short writes are retried. - Duplicate keys log a warning; an empty file value cannot clear a default. - sentinel_host_test runs the constructor-then-main order, the non-regular-file paths, the log-failure paths and a run that captures every logcat and disk-log line and asserts no configured value appears in any of them. Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
…nsports (refs #158) Owns the per-connection state machine the PC bridge keeps inside ws_bridge.cpp: connection identity (config/login/matchmaker), once-per-session login injection ahead of queued frames, remote end closing every game socket on the session and resetting numbering, size limits, bounded pending/outbound queues and backpressure. Transports, the login builder and the log sink are injected and never called under the router lock. Host tests with fake transports run in `just test-quest-router`, wired into `just verify`. Co-Authored-By: Andrew Bates <a@sprock.io>
…refs #158) The game-facing transport for the shared session router: a POSIX listener on an ephemeral 127.0.0.1 port, the HTTP upgrade handshake, an incremental frame decoder (partial reads, fragmentation, size limits checked on the header) and a non-blocking, bounded write path that reports backpressure to the router. Host tests drive it with a raw TCP client, including one-byte writes. Co-Authored-By: Andrew Bates <a@sprock.io>
…nd make the rewrite atomic (#158) Review of the first cut found it changed [CNSUser+0x88], which CNSOVRUser never reads: the sender gets the account id from a virtual AccountID() that returns a process global, and the platform nibble is already 4. The rewrite now writes that global after checking the three instructions of CNSOVRUser::AccountID() and proves the wire value by making the same virtual call. - the JSON rewrite snapshots and restores every key it touched on any refusal or exception; the account id is restored with it - no password in the login JSON: the game logs the outgoing login and only clears access_token, nonce, authticket, authcode, authtoken, userhash; the server reads the password from the upgrade URL, not the JSON - CJson is resolved from libpnsovr.so (no DT_NEEDED on libr15, own CJson) - the headset's real system_info values are kept - installs through the merged GotHook/CallbackThunk (atomic original, mutex) - structured key=value log lines through the sentinel sink - ADR 0003: libpnsovr build id row, login interception section Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
… 20-digit literal (refs #158) The N20 identity-literal sensor reads a 20-digit number in src/runtime as an account id. Co-Authored-By: Andrew Bates <a@sprock.io>
…ption contract, JSON logs (refs #158) Review of #222: one lock across WriteSlot (two handles on one RELRO page crashed), Remove and Install are compare-and-swap, protection to restore is read under the lock, thunk lets the original's exceptions through and never re-calls the original, atomic publish, fixed-size registry and pthread mutex (no throw from the ELF constructor), GotHook::Forget, JSON log lines, no agent paths in the repo test. Co-Authored-By: Andrew Bates <a@sprock.io>
…l TLS, session bridge (refs #158) ConnectorRemoteTransport implements the router's RemoteTransport over an abstract WsConnector: wss:// only, one connect attempt per session, no retry and no downgrade after a failure, no URL or header value in any log line. CurlWsConnector implements it with libcurl's WebSocket API over the OpenSSL the Quest manifest already provides (adds the curl `websockets` feature): peer and host verification pinned on, TLS 1.2+, wss-only protocol allow-list, no redirects, trust from the Android system CA directory. SessionBridge wires loopback server, router, transport and connector and reports the loopback port for the shared redirect. Host tests: a fake connector under the real router, and `just test-quest-tls` against real TLS servers (right chain, wrong CA, wrong host, self-signed, empty trust store, non-TLS server, ws://) asserting no plaintext fallback. A verify sensor pins the connector's TLS options. Co-Authored-By: Andrew Bates <a@sprock.io>
… a wrong shape, tidy comments (refs #158) - RefreshOutcome::Denied for HTTP 400/401/403 so callers can stop retrying a token the server refuses; other non-200 stays Rejected. - The wider json::exception catch now logs 'valid JSON of an unexpected shape' for [] or {"access_token":1} instead of 'not valid JSON'. gameserver.cpp's RefreshAuthToken call picks up the same catch. - HttpClient::Interrupt() so a shutdown can abort a request in flight. - The device-flow log says open_result= (it was shellexecute_result= on every platform). - RefreshRequestBody test now calls BuildRefreshBody; comments describe the current tree; the refresh lead / fallback relationship is stated once and matches the static_assert. Co-Authored-By: Andrew Bates <a@sprock.io>
… handle dying credentials (refs #158) - CA: read the Android CA directories into memory and pass them as CAINFO_BLOB; CAPATH cannot find Android's old-hash file names with an OpenSSL backend. Fails closed, loudly, with no certificate. Proxy variables ignored, response size capped, every setopt checked, requests interruptible. Loopback TLS tests with a generated CA and an Android-style directory. - The refresh token goes to /data/data/<package>/files (derived from /proc/self/cmdline); only the login link stays on /sdcard. O_NOFOLLOW, exclusive temp file, directory fsync. - A permanent poll error gives up after a bounded run of failures and logs the status. A denied or expired refresh token publishes Expired and restarts the device login. - Session: Start/Stop serialised, no lock held across the log sink, Start does not throw, Stop does not wait out a request. Tests run under a per-test deadline. - N94 sensor pins the core-to-EchoVR log level mapping. Co-Authored-By: Andrew Bates <a@sprock.io>
Co-Authored-By: Andrew Bates <a@sprock.io>
…ude-quest-hooks' and 'origin/claude-quest-tokenauth' into claude-quest-router * origin/claude-evr-codec: test(compat): build the expected wrapped length instead of spelling a 20-digit literal (refs #158) fix(just): test-quest-shared prints its missing-header message instead of dying in the glob (refs #158) fix(compat): check truncation before decoding in the game->server walk; tighten codec tests (refs #158) * origin/claude-quest-hooks: fix(quest): serialize GOT writes process-wide, compare-and-swap, exception contract, JSON logs (refs #158) * origin/claude-quest-tokenauth: docs(adr): record the shared token-auth core and the Quest adapters (refs #158) test(quest): let auth_core_test run a single test by name filter (refs #158) feat(quest): token auth session over the shared auth core, libcurl over OpenSSL on arm64-android (refs #158) refactor(token-auth): extract the platform-neutral auth core (refs #158) # Conflicts: # docs/adr/0003-quest-networking-port.md # justfile # src/quest/vcpkg.json
…quest-login * origin/claude-quest-hooks: fix(quest): serialize GOT writes process-wide, compare-and-swap, exception contract, JSON logs (refs #158)
…haviour (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
… type (#158) Second review of the login rewrite: - buildversion, appid, lobbyversion and publisher_lock are never written. The server classifies the client on them (IsPCVR() is buildversion != 630783, which selects the Quest encoder flag layout on lobby join); the PCVR constants would have made a Quest login a PCVR one. The Quest sends 630783 itself. - the account id stays installed after the send (LogInSuccessCB compares it with the server reply); RewriteAndSend ties the send to the rewrite and a test fails if a restore-after-send is added - the snapshot models absent, null, string, int and boolean and refuses to write over real, array or object; a null is restored as a null - CJson functions are bound only if libpnsovr's export and its own PLT slot both equal the pinned address - the CNSOVRUser check is the exact vtable pointer - structured log records (event, outcome, fields) through the sentinel JSON sink; a missing identity source is logged - tests use QCHECK and no longer abort on a missing key - ADR 0003: the identity/client-class decision, the CJson type rules, who writes the account-id global and what reads it Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
…ude-quest-router * origin/claude-quest-tokenauth: docs(adr): Quest token-auth trust, credential location and session behaviour (refs #158) fix(quest): trust Android's CA store, keep the refresh token private, handle dying credentials (refs #158) fix(token-auth): classify a refused refresh token, tell bad JSON from a wrong shape, tidy comments (refs #158)
The pre-write type check only matters on a build whose SetInt replaces a real instead of refusing it. The fake can now model that, and the test fails if the check is removed. Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
…hared loader, not CAPATH (refs #158) Token auth (#223) found OpenSSL cannot use Android's /system/etc/security/cacerts as a CApath (old MD5 subject-hash names). CurlWsConnector now loads the same directories with quest/auth/ca_bundle into memory and passes CURLOPT_CAINFO_BLOB; with no certificate loaded every Connect fails closed (TlsError, kNoTrustAnchors). The host TLS test trusts directories through the same loader; the verify sensor requires CAINFO_BLOB and forbids CAPATH. Not built or run: the host is under the disk floor. Co-Authored-By: Andrew Bates <a@sprock.io>
…quest-redirect * origin/claude-quest-hooks: fix(quest): serialize GOT writes process-wide, compare-and-swap, exception contract, JSON logs (refs #158) test(quest): lifecycle gtests, pinned-ELF resolution, wire quest hooks into verify (refs #158) test(quest): host tests for the GOT backend and callback thunks (refs #158) feat(quest): exact GOT backend, shared hook lifecycle header, typed callback thunks (refs #158)
…-quest-redirect * origin/claude-quest-config: fix(quest): keep the resolved config past static init, bound the config read, log structured fix(quest): satisfy the shared-source sensors for the Quest config feat(quest): embedded-default config, nevr-quest.json override and activation switches # Conflicts: # src/quest/sentinel/CMakeLists.txt
…xports, one hook library (refs #158) Second review of #222: handler that swallows the original's exception and then throws is a handler fault (exception identity, not a sticky flag); the entry uses one captured original per call and the rollback after a failed re-protect keeps the original published; per-thread state moves from emulated thread_local (allocates on first use) to pthread keys; streaming relocation scan (no 8-slot cap); protect seam + tests for failed re-protect, compare-and-swap Install and the 64-slot bound; -fvisibility=hidden with an export allowlist test; got_hook compiled once into nevr_quest_got_hook with a justfile sensor; first-call-only log in the clock hook. Not built or run on this commit: the disk was under the 8 GiB floor. Co-Authored-By: Andrew Bates <a@sprock.io>
…quest-login * origin/claude-quest-hooks: fix(quest): thunk exception scoping, pthread-key call state, hidden exports, one hook library (refs #158)
… it again (#158) nevr_quest_login compiled the backend sources itself; the backend now builds once as nevr_quest_got_hook and test-quest-hooks fails if a second Quest CMake list names the source. The login targets are defined after the sentinel and only when that target exists. Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
…quest-router * origin/claude-quest-hooks: fix(quest): thunk exception scoping, pthread-key call state, hidden exports, one hook library (refs #158)
…check, static-init sensor Second-round review of the Quest config: - The on-disk log opens non-blocking and must be a regular file, so a FIFO at nevr-sentinel.log cannot stall the constructor. A torn record leaves its fragment on its own line; a log at 1 MiB is renamed, never deleted. - File warnings are capped at 32 lines plus one suppression line and repeated duplicate keys collapse per name. Key names that are not lowercase identifiers are not echoed. - The resolution catch block allocates nothing (EmitFixed). - sentinel_host_test checks every logcat line when it is emitted, over rejected, malformed and non-regular inputs too; its constructors use priorities so the static-init and timeout checks do not depend on link order. - tools/check_quest_static_init.sh fails build-android when a sentinel object gains a dynamic initializer; activation.cpp and sentinel_log.cpp compile with -Wall -Wextra -Werror; ADR 0003 contract 4 states the constructor's I/O limits. Refs #158 Co-Authored-By: Andrew Bates <a@sprock.io>
The login socket is silent until the game's LogInRequestv2. Releasing the hold restarted the first-frame idle clock, so a login socket was closed 1008 thirty seconds after the player signed in if the game had not yet sent its login. The router now tells the transport (GameTransport::SetIdleExempt, renamed from SetHeld) that the login socket is exempt from the first-frame idle close from the moment it is the login socket until it stops being it. Config, matchmaker and unclassified sockets keep the idle close. The release-restarts-the-clock logic and its test are replaced by a test that a released login socket stays open past several windows while a silent config socket is closed. Co-Authored-By: claude-main <agents@sprock.io>
…ilure (#239) The login gate mapped every Failed state to Refused, so a failure the session retries every recovery period (a flaky network, a refused refresh) closed the held login socket 1011 and the game's reconnects failed fast into "service unavailable". The session snapshot now says whether a Failed will be retried (Snapshot::will_retry, set for the recoverable branch only). The gate answers Awaiting for a recoverable Failed, for Expired (the token is being replaced) and for a Ready whose token ran out (a refresh is under way); only a final failure, a stopped session or a token without an account is Refused. Co-Authored-By: claude-main <agents@sprock.io>
…tstanding (#239) An STcpConnectionUnrequireEvent lowers the request count of the connection it arrives on. The service sends it as a frame of its own after the reply, from concurrent goroutines, so frames interleave, and routing it to the connection of the previous message could hand the profile update's Unrequire to the matchmaker that a ping had just reached. The router now counts the requests each connection sends on the shared login session and delivers each Unrequire to a connection with a request outstanding, the login connection first, never lowering a count below zero. An Unrequire inside a batched frame lowers the count of the connection the frame goes to. With nothing outstanding it follows the message the service started itself (a LobbyPingRequest), else the last message. The lobby requests are counted although it is not established that each sets the require flag. Co-Authored-By: claude-main <agents@sprock.io>
…prerequisites (#239) The signed-in notice ("Select RETRY") was published at verification, but the flag the page-enable hook used (awaiting the player) and the prerequisites' Ready() flag turned over only on the 2 s token-auth poll, and they were two flags. A RETRY in that window had its logging-in page skipped while the login could still succeed, stranding the player on the login-failed screen while logged in. Both now read one word, QuestLogin::attempt_gate, which the token-auth source publishes: the logging-in skip stops in the same instant the login may proceed. An attempt whose logging-in page was skipped is poisoned and fails its prerequisites until the game leaves logging in, even if the gate turns ready first. The notice is applied to the block only once the login may proceed (on a prompt screen and on one that shows the game's own text). SetAwaitingPlayer and its seam are gone. Co-Authored-By: claude-main <agents@sprock.io>
…239) The latch flag is recomputed on the game thread only (Update, and right after the error text hook), so a genuine error written by the game's unhooked SetErrorMessage in the same frame as the page enable still looked like the sentinel's text, and its error page was dropped. The page-enable hook now hashes the live block (lock-free, relaxed byte loads) against the latched hash before it skips anything. A sequence counter around the sentinel's own block writes keeps a half-written block, or a read that straddled a write, from reading as a genuine error. Co-Authored-By: claude-main <agents@sprock.io>
…#239) A local login failure makes the game send its own error event right after SetDelimitedErrorMessage. A prompt published in the same frame was then rewritten by the Update hook, which sent a second event with nothing to space it from the game's: two events in one frame. The error text hook now stamps the spacing clock when it follows a failure, so the rewrite's event waits out the spacing. Co-Authored-By: claude-main <agents@sprock.io>
…ing requests (#239) The game's outstanding-request count is 8 bits; an Unrequire with none outstanding wraps it to 255 without a sound, and the login connection's later loss then raises "connection lost" (-95). The previous routing gave an Unrequire to a connection that merely had a count, counted the wrong set of requests, and sent a ping with no matchmaker to the login connection. The router now keeps the game's own count per connection from the flagged requests (RE: every login send but LogOut, TelemetryEvent and RemoteLogSetv3; every matchmaker send but MatchmakerStatusRequest; SNSConfigRequestv2) and queues, per message the service pairs with an Unrequire (ChannelInfoResponse, DocumentSuccess, UpdateProfileSuccess, LobbyPingRequest), the connection it went to. Each standalone Unrequire takes the queue's front and is dropped, and counted, if that connection has nothing outstanding. An Unrequire inside the login reply frame lowers the count of the connection the frame goes to. ChannelInfoResponse goes to the login connection; a LobbyPingRequest with no matchmaker is dropped with its Unrequire. The config connection's own remote follows the same rule. Co-Authored-By: claude-main <agents@sprock.io>
Nakama sends every login failure as a LoginFailure frame and then a standalone Unrequire. LoginFailure was missing from the messages the router pairs with an Unrequire, so that Unrequire was dropped and the login request stayed outstanding: a later Unrequire nothing was owed went through and wrapped the game's count. Co-Authored-By: claude-main <agents@sprock.io>
…ep (#239) The page-enable hook decided to skip the logging-in page from a read of the gate, then poisoned the attempt in a second step. If the gate turned ready in between, the page was skipped and the attempt poisoned although the login could proceed: the player stranded on the login-failed screen, then failed by the attempt's own prerequisites. The poison is now one compare-and-swap on the gate word that only succeeds while the gate is not ready; if it fails the page goes through. The flip is injected between the read and the swap in the test. Co-Authored-By: claude-main <agents@sprock.io>
LatchHoldsLive hashes the error block with relaxed byte loads and then re-reads the write sequence to tell its own write from a genuine one. On arm64 the relaxed loads may be reordered after that sequence load, so a read that straddled a write could still look genuine. An acquire fence now sits between them. This is not testable on the x86 host (TSO); the existing worker-thread test still passes. Co-Authored-By: claude-main <agents@sprock.io>
The router drops an Unrequire on purpose when its connection has nothing outstanding, but only a rate-limited router line showed it. The token-auth poll now logs router_unrequire_dropped (total and increase) each time IntegratedBridge::DroppedUnrequires changed. The reporter's counter table has 46 of 48 slots in use, so this is a structured line, not a counter. Co-Authored-By: claude-main <agents@sprock.io>
An Unrequire cannot be taken out of a frame. When the connection a frame goes to has nothing outstanding for the Unrequire inside it, it reaches the game and wraps the game's count; the embedded-Unrequire loop stopped without recording it. It is now counted (Stats::unmatchedEmbeddedUnrequires, also for an Unrequire inside a frame the router drops) and reported in the router_unrequire_dropped line. ADR 0003 states the limit: with nakama's DisableLoginMessage branch (failure and Unrequire, then a login that still succeeds) the success frame's embedded Unrequire arrives after the failure's covered the login request. Correction to 5dec746: before it, the Unrequire after a LoginFailure was dropped and the login count stayed at 1. A later loss of the login socket then took the "connection lost" (-95) path instead of a silent reconnect; the count did not wrap. Co-Authored-By: claude-main <agents@sprock.io>
#239) ShutdownIntegration deleted the bridge, then stopped and joined the token-auth thread, whose poll loop loads rt.bridge and calls into it every 2 s: a use after delete if a poll ran in between. The thread is now stopped and joined first, then the bridge is stopped and deleted, then token auth. Not covered by a host test: production_steps.cpp is built only into the Android sentinel and ShutdownIntegration is not reachable from the host tests; build-android compiles it. Co-Authored-By: claude-main <agents@sprock.io>
Lands the eight Quest PRs (#219 #220 #221 #222 #223 #226 #227 #229) and the #239 fix, as tested in smoke #4, on top of main. Conflict resolutions: - justfile build-android: main bounds the job count, the Quest branch ran the static-init check in the same recipe. Kept main's line; the check moved to its own recipe, check-android-static-init, which test-android (and so the android workflow) runs. A check inside build-android would fail main's test_build_android_jobs, which runs the recipe with a fake cmake. - justfile test-auth-unit: the branch's run_test wrapper and test_evr_codec, without test_winhttp_stub, which main removed (23059c9). - justfile verify: the branch's Quest suites and timeout, with the union of both sides' python sensor modules. - tools/tests/test_executable_scripts.py: main's version (every tracked shebang file is 100755, no exception list) supersedes the branch's frozen list. - src/core/auth_token.h: the branch's SerializeCredentialsJson call; main's comment about the 60s load-path cap goes on SerializeCredentialsJson. - src/modules/token-auth/src/token_auth.cpp + src/core/device_auth_flow.*: main's #202 tolerance of 5 consecutive poll errors is ported into the shared nevr::auth core as DeviceFlowOps::max_consecutive_poll_errors; the Windows module sets 5, Quest keeps the default 1 (its tested behaviour). - src/runtime/compat/ws_bridge.cpp: main's #201 MoveCodeLineFirst kept in the bridge, on EvrCodec's constants; the platform/bearer helpers stay in EvrCodec; the login-injected line keeps the branch's empty-request guard and gains main's ConnLabel. - src/runtime/tests/test_token_auth.cpp: both includes. - tools/tests/test_naming.py: the ws_bridge.cpp "nEVR" legacy entry is gone with the comment that held it. Co-Authored-By: claude-main <agents@sprock.io>
Quest keeps DeviceFlowOps::max_consecutive_poll_errors at 1. Its poll op
returns every transient failure (503, 429, no connection, a 200 the parser
cannot read) as Pending and polls on to the code's deadline, so the core
only sees Error for the server's own refusal, which is final.
- seven transient failures of every kind, then verified: one code, a login
- three transient failures, then {"error":...} or a 403: the login ends on
that poll (4 polls), not after further retries
Mutation: returning transient failures as Error fails the first; setting
the Quest limit to 5 fails the second (8 polls, not 4).
Co-Authored-By: claude-main <agents@sprock.io>
…#239) Co-Authored-By: claude-main <agents@sprock.io>
This was referenced Oct 10, 2026
feat(quest): embedded-default config, nevr-quest.json override and activation switches (#158 C)
#220
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the Quest work on main as one unit:
claude-quest-integration-2(tip0074d35b, the code that passed smoke #4 on the Quest) merged withorigin/main.Closes #239
Supersedes #219 #220 #221 #222 #223 #226 #227 #229
Conflict resolutions (
eae29b1b)justfilebuild-android${CMAKE_BUILD_PARALLEL_LEVEL:-4}and tests that withtools/tests/test_build_android_jobs.py, which runs the recipe with a fake cmaketools/check_quest_static_init.shin the same recipecheck-android-static-init, whichtest-android(and so.github/workflows/android.yml) runs. Left insidebuild-android, it fails main's test because the fake cmake produces no library.justfiletest-auth-unittest_winhttp_stub(23059c9e)run_testwrapper (timeout + 4G cap), addedtest_evr_codectest_evr_codec, withouttest_winhttp_stubjustfileverifytest_version_reconfigure,test_vcpkg_pin,test_android_workflow,test_build_android_jobs,test_naming,test_server_hold)test-quest-*),timeout -k 10 600,test_check_quest_static_init,test_quest_standin_testonlytools/tests/test_executable_scripts.pysrc/core/auth_token.hSerializeCredentialsJson(core/auth_token_model.h)SerializeCredentialsJson.src/modules/token-auth/src/token_auth.cpp,src/core/device_auth_flow.{h,cpp}nevr::auth::RunDeviceCodeFlow, which aborts on the first errorDeviceFlowOps::max_consecutive_poll_errors, with main's log lines. The Windows module sets 5 (main's behaviour). Quest keeps the default 1; see "Quest poll-error tolerance" below.src/runtime/compat/ws_bridge.cpp(3 hunks)MoveCodeLineFirst;ConnLabelin the login-injected line; comment cleanups (#151)EvrCodec; an empty-request guard on login injectionMoveCodeLineFirststays in the bridge, onEvrCodec::kHeaderSize/kSymLoginFailure. The helpers stay inEvrCodec. The login line keeps the guard and gainsConnLabel.src/runtime/tests/test_token_auth.cpp#include "core/bounded_retry.h"#include "core/auth_refresh.h"tools/tests/test_naming.py(not a textual conflict)ws_bridge.cppas holding legacy "nEVR"Later commits on the branch:
813912b5mergesb25092d8(AGENTS.md, one line).19effee7test(quest): pins the device-code poll failure policy (see below).0f9e93ecmergesd473ee68(diag(mic): count the game's MicAvailable/MicRead calls (#95) #325,src/runtime/patch/mic_provider.cpp). The merge is clean. Final head:0f9e93ec.Quest poll-error tolerance (decided: Quest stays at 1)
The Windows module sets
DeviceFlowOps::max_consecutive_poll_errors = 5(#202). Quest keeps the default 1. Quest already has #202's outage tolerance, and more of it: its poll op (src/quest/auth/session.cpp,Session::RunDeviceCode) returns every transient failure asPending, including 503, 429, 408, no connection, and a 200 the parser cannot read, and it polls on until the code's own deadline. The core only receivesErrorfor the server's own refusal (an{"error":...}body or a non-transient 4xx), which is final. A limit of 5 would only re-poll a refused code. The new tests insrc/quest/tests/auth_core_test.cpppin this:session_seven_transient_poll_failures_then_verified_end_in_a_login: 503, 429, no transport, an unparseable 200, 503, no transport, 429, then verified. Result: Ready, 8 polls, 1 code, 1 link shown.session_a_refusal_after_transient_failures_ends_the_login_on_that_poll: 3×503 then{"error":"bad code"}, and 3×no-transport then 403. Both end Failed after exactly 4 polls./var/tmp/work-nevr-runtime/claude-quest-land/). Returning transient failures asError(mutation-quest-M1-transient-as-error.log) fails the first test, plus the existing outage tests. Setting the Quest limit to 5 (mutation-quest-M2-limit-5.log) gives fourCHECK_EQ failedlines. Log lines 6 and 7 areauth_core_test.cpp:1545: http.Count("poll") == 4, the new refusal test, once per case. Log line 5 is:1445: http.Count("poll") == 1(session_a_refused_poll_ends_the_login_at_once_and_is_not_retried). Log line 62 is:2111: http.Count("poll") == 1(the existing{"error":"bad code"}case); that line'sFAILlabel is spliced with another test's name because the test output is interleaved. With the source restored, all pass.Evidence (final head
0f9e93ec)just verify, capped (VCPKG_MAX_CONCURRENCY=6 CMAKE_BUILD_PARALLEL_LEVEL=6 systemd-run --user --scope -p MemoryMax=10G). Log:/var/tmp/work-nevr-runtime/claude-quest-land/verify-0f9e93ec-20261010T023711.log. First line:verify head: 0f9e93ec6b2239bd8c8c6d24e52250b5b70b5d9d. Last line:verify: OK (mingw-release). The GTest count is 698, and Python runs 178 then 353 tests, all OK. Two pre-existinghook-invariants: WARN [N83] known self-collisionlines appear. Both new Quest tests showPASS../launch-client.sh --dll <worktree>/build/mingw-release/bin/BugSplat64.dll --exit-after-login, capped, onXephyr :101(DISPLAY=:101,WAYLAND_DISPLAYunset). The DLL was built at0f9e93ec, sha2564613249becb05cd617ed540aead46baa79d06619bb6198e2b053c9906bb81d91. Script output:PASS: client reached logged in,original BugSplat64.dll restored and verified, rc 0 (/var/tmp/work-nevr-runtime/claude-quest-land/pc-login-0f9e93ec-20261010T023651.log). From the game's own log, in the game root the script uses (the main checkout,launch-client.sh --print-game-root→/home/andrew/src/nevr-runtime),/home/andrew/src/nevr-runtime/echovr/.wineprefix/drive_c/users/andrew/AppData/Local/EchoVR/logs/nevr-2026-10-10T07-36-52.463.jsonl::84[NEVR.CONFIG] built-in defaults embedded in this build: nevr_socket_uri, nevr_http_uri, nevr_http_key, nevr_server_key:168[NEVR.WS] login injected xpid=OVR-ORG-1540343945437909062 platform=4 conn=1 (login) size=1096:169[NEVR.WS] LOGIN SUCCESS:186[EVR] [NETGAME] NetGame switching state (from logging in, to logged in)just test-android(capped) givescheck_quest_static_init: OK (3 .init_array entries ..., 9 object files)andok github.com/EchoTools/nevr-runtime/tests/quest(android-0f9e93ec-20261010T024145.log).0f9e93ecwithjust android-repack-apk /mnt/games/cache/r15_goldmaster_store.apk ... <smoke-4 keystore>, signer SHA-256bc4d88e4c523f2516de3c32be63084a4fb0fe8f368d773f71b3d4bdf45b8517a. File:/var/tmp/work-nevr-runtime/claude-quest-land/r15_nevr-sentinel_signed-0f9e93ec.apk, sha25652b8ede1bec9866c157b7e9cd987dfeaffd78155484242de661b774e6d824dd2.eae29b1b): with the Windows limit set to 1,DeviceAuthFlow.TransientPollErrorsAreRetriedUntilVerified,ConsecutivePollErrorsEndTheWaitAfterTheLimitandAnAnsweredPollResetsTheErrorRunfail (mutation-poll-errors.log,3 FAILED TESTS). With the source restored, 3/3 pass (mutation-poll-errors-restored.log).Not run
Branch lifecycle: deleted when this PR merges or closes.
🤖 Generated with Claude Code