Cellular support, background keep-alive, and location-simulator improvements#432
Open
dizzafizza wants to merge 37 commits into
Open
Cellular support, background keep-alive, and location-simulator improvements#432dizzafizza wants to merge 37 commits into
dizzafizza wants to merge 37 commits into
Conversation
made the default speed walking speed
…g/bus/custom) Adds a SpeedProfile picker to route simulation with fixed paces for walking, jogging, and cycling; road-speed-limit-based driving; a capped bus profile that slows on approach to and dwells at Overpass bus_stop nodes; and a custom km/h speed entered by the user. Also extends OverpassResponse.Element with lat/lon so bus stop nodes (which report coordinates directly rather than via geometry) can be parsed.
Leftover empty-array return from the pre-refactor signature caused 'cannot convert [Any] to RouteSpeedContext' at MapSelectionView.swift:338.
…ic, transit stops) - New toolbar menu on the location simulator map to switch between Standard, Satellite, and Hybrid map types - Traffic overlay toggle for Standard/Hybrid modes - Points-of-interest mode: All Places, Transit Stops (surfaces bus stops), or Hidden - Selections persist across launches via AppStorage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUJbpUQiRUbjiTiYA4v7UK
The bus stops fetched from OpenStreetMap for bus-mode pacing/dwell were only used to compute playback timing and never drawn. Surface them from the prefetch result into view state and render them as orange bus markers whenever the Bus speed profile is active.
Previously only OSM nodes tagged highway=bus_stop were fetched and drawn, which misses most stops in regions that use the newer public_transport schema (platform / stop_position with bus=yes). Query and decode all of these tag combinations so bus mode surfaces the full set of stops instead of the sparse few tagged the legacy way.
Long routes took minutes to prepare (or appeared stuck) because of three compounding costs, each now fixed: - Overpass fetch: the query covered the route's whole bounding box, so a long or diagonal route pulled every speed-limited road in a huge rectangle. Replace it with an 'around' corridor query over a downsampled route line (75 m spacing, capped at 700 points), so the payload scales with route length, not bounding area. POST the query (corridor strings outgrow GET URLs) with an explicit 30 s timeout so a slow server can no longer stall route prep indefinitely — failures fall back to profile/ETA pacing as before. - Speed-limit matching: nearestSpeedLimit rescanned every OSM way segment for every route segment (O(route x ways), tens of millions of distance checks on long routes). Add RouteSpeedIndex, a grid-bucketed spatial index (~440 m cells) probed via its 3x3 neighborhood; every search radius is well below one cell, so results are unchanged. The bus-stop approach check and the dwell snapping pass (previously O(stops x samples)) use the same bucketing. - Route densification: 10 m sampling made a 300 km route produce 30k+ points on the main thread. Sampling now widens adaptively past a 25k point cap; playback interpolates per tick within segments anyway, so smoothness is unaffected.
When you enable JIT for an app and switch to it (e.g. a game), StikDebug gets backgrounded. If iOS suspends it, the debug connection it holds — and the marco/polo heartbeat that keeps that connection trusted — goes idle, the device tears it down, and the app loses its session after a while. The app also assumed Wi-Fi even though the tunnel rides LocalDevVPN and works on cellular. Session survival (backgrounding): - DebugKeepAliveLease now forces the silent-audio and background-location keep-alive on for the whole debug session, regardless of the user's keep-alive toggles, so a session survives being switched out even if those toggles are off. - Stop the background-task expiration handler from tearing the whole keep-alive down. It used to call invalidate() (killing audio/location) the moment the ~30s assertion elapsed; now it just renews the assertion while the silent-audio keep-alive continues to sustain background execution, so a long backgrounded session keeps running instead of dropping. - Play an inaudible non-zero signal instead of digital silence so iOS does not treat the audio session as idle and reclaim it (suspending the app). Cellular support: - Add NetworkPathMonitor (NWPathMonitor) so the app observes the real path (Wi-Fi, cellular, or VPN) instead of assuming Wi-Fi. - Retry the tunnel connect with backoff so transient cellular failures recover instead of erroring, skipping permanent errors (bad/missing pairing file, invalid target IP). - Reconnect the tunnel on network-path changes (Wi-Fi<->cellular handoff). - Remove the false "Wi-Fi required" wording from the connection alerts and the README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qznh32XffG6vwFLMcwm1Mq
Targets the real problem: an app (e.g. Roblox) getting closed/disconnected by
iOS once it's in the background. iOS won't let one app keep another alive —
except that a process a debugger is attached to is not suspended on
backgrounding (the same reason an Xcode-run app keeps executing in the
background). This uses that.
- JITEnableContext.keepAppAlive: launch the app suspended, vAttach (which sets
CS_DEBUGGED so JIT is on and marks the process debugger-owned), disable its
memory limit to reduce background jetsam kills, send a raw continue so it
runs, then hold the debug connection open with a heartbeat until the user
stops it — then interrupt + detach so the app keeps running on its own. The
hold is a single-threaded sleep-poll (no blocking socket reads, no concurrent
handle access), so it's cleanly cancellable.
- BackgroundAliveManager + HoldToken: own one active hold, keep StikDebug itself
alive via DebugKeepAliveLease for the duration, publish the active app name.
- UI: an experimental Settings toggle ("Hold App Alive in Background"); when on,
tapping an app in Home holds it alive instead of a normal one-shot JIT, and a
banner with a Stop button appears while a hold is active.
Caveats (documented in-app): experimental and unverified on-device; requires
StikDebug to stay running, so it's best-effort not bulletproof; cannot revive
an app iOS already killed; higher battery use.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qznh32XffG6vwFLMcwm1Mq
debug_proxy_send_raw expects a UInt (uintptr_t) length; packet.count is Int, which does not auto-convert. Archive failed with "cannot convert value of type 'Int' to expected argument type 'UInt'". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qznh32XffG6vwFLMcwm1Mq
When rp_pairing_file_read fails to parse the local pairing plist (e.g. an older/incompatible file missing public_key), the raw Serde/plist debug string was surfaced as a generic "Connection Error" with a useless "Try Again" button — retrying a local parse failure can never succeed. Reclassify this failure to the same error code used for invalid pairing files so it routes to the existing "Select New File" prompt instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAnD7odyVzX36xY1vvZcJL
Fix cryptic error for pairing files missing required fields
…tion-n934o5 Revert "Fix cryptic error for pairing files missing required fields"
swift.yml runs 'swift build' / 'swift test', which require a Package.swift. This repo is an Xcode app project with no Swift package, so the workflow has failed with 'Could not find Package.swift' on every run since it was added, marking every push and PR with a failing check. The Build Debug IPA workflow already compiles the project via xcodebuild on the same triggers, so build validation is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
The route-speed spatial index skipped any OSM way segment whose bounding box spanned more than 8 cells (~3.5 km). Sparsely-noded rural highways can legitimately run much farther between geometry nodes, so those stretches were dropped from the index and silently fell back to average route pacing instead of their real speed limit. Raise the guard to 128 cells (~57 km at the equator), which still catches genuinely broken data such as antimeridian jumps (~90,000 cells) while keeping realistic long segments in the index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
Documents the review of all fork changes since upstream divergence, the two defects fixed (broken swift.yml CI workflow, over-aggressive speed-index segment cap), and the areas verified as correct. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
Remove Swift Package CI workflow that can never pass
Added model details
The route corridor was sent to Overpass as one all-or-nothing query that embedded the coordinate string four times (roads + three bus-stop tag clauses). On long routes that regularly exceeded the 25s server timeout, and because the caller swallowed errors, every bus stop and speed limit was silently lost — exactly the 'stops never load' failure. The 700-point corridor cap also widened point spacing far beyond the 90m search radius on very long routes, cutting corners past stops even when the query succeeded. Rework the fetch into independent corridor chunks (~12km each, two in flight, per-chunk timeouts): - Each chunk is a small, fast request; a failed chunk only loses its own stretch instead of the whole route, and failures are logged. - Bus stops stream onto the map as chunks arrive via an onPartialBusStops callback instead of appearing only at the end. - The three bus-stop tag clauses are collapsed into one regex clause (a single corridor pass server-side); exact filtering stays client-side in tagsDescribeBusStop. - Results are deduplicated across chunk seams by OSM element id, and stops mapped twice (platform + stop_position a few meters apart) are merged spatially so markers and dwells aren't doubled. - Corridor spacing now stays within the search radius up to ~285km of route (24 chunks x 160 points) instead of degrading past ~50km. Verified the exact generated query shape against the live Overpass API: HTTP 200 in ~2s, element ids/geometry/lat-lon match the decoder, and the regex clause returns both legacy and public_transport-schema stops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
Load bus stops in chunks so long routes stop timing out
… stops Bus stops loaded inconsistently even after the previous chunking fix. Investigation against the live Overpass API found two compounding root causes: 1. Overpass's around(radius, point-list) operator costs scale with points-in-list x candidate-elements-in-area. A single chunk at the previous chunkMaxPoints=160 reliably timed out; even much smaller chunks (25-60 points) took anywhere from 3s to 23s depending on how road-dense the area was -- the same chunk size that was fine in a quiet area could time out in a busy one, which is exactly the "sometimes works, sometimes doesn't" behavior reported. 2. When Overpass's own [timeout:N] elapses, it replies HTTP 200 with an empty elements array and a "remark" field describing the error -- not an error status. The client only checked the HTTP status code, so a timed-out chunk was silently indistinguishable from "no stops near this stretch of road" and never retried or logged. Fixes: - Replace the around() query with a bounding-box query per chunk. A bbox test is an O(1) rectangle check per candidate regardless of route shape or point density, unlike around(). Verified live: a chunk that took 16-23s (or timed out) via around() resolved via bbox in 2-8s for the same area, including a deliberately pathological zigzag route whose bbox came out much larger than the route itself. - Chunk the route by real-world distance (~5km) instead of point count, since query cost no longer depends on point density -- this also simplifies chunk building (no separate query-string downsampling step). - Decode the "remark" field and treat any non-nil value as a failure so a server-side timeout is retried and logged instead of silently accepted as empty data. - Retry each chunk once with a short backoff before giving up, since the public Overpass instance is a shared, load-dependent resource where a single slow response shouldn't cost that stretch of road its data. Verified the exact query shape and decoder assumptions against the live Overpass API across multiple route shapes (realistic 5km stretch, dense-area chunk, pathological zigzag) -- all resolved in under 8s with correct bus stop / speed limit extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
Fix inconsistent bus stop loading: switch route queries from around() to bbox
…zoom Bus stop markers are our own Marker overlay drawn from Overpass data, not Apple's native POI layer, so setting Points of Interest to "Hidden" (which only sets MapStyle's pointsOfInterest category filter) had no effect on them -- they kept showing regardless of the setting. Separately, a bus route can have dozens of stops close together; zoomed out they overlap into an unreadable stack of pins (see reported screenshot: a wall of stacked bus icons across a multi-city view). Fixes: - Gate the bus stop markers on mapPointsOfInterestMode != .hidden so "Hidden" actually hides them. - Track the map's visible latitude span via onMapCameraChange and thin markers through the existing dedupedBusStops spatial merge, using a spacing that scales with the visible span -- full detail at close zoom, progressively fewer markers as the map zooms out. The full-fidelity routeBusStops list (used for dwell pacing during playback) is untouched; only the on-screen marker set is thinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
The experimental "Hold App Alive in Background" mode attached the debugger (setting CS_DEBUGGED) but never ran the app's assigned JIT script. On TXM / iOS 26+ devices JIT is not enabled merely by attaching: the app requests executable memory at runtime via `brk #0xf00d` syscalls that the debugger must service, which is exactly what the JIT script does in its breakpoint loop. Without the script those syscalls went unhandled, so the app trapped and hung (or JIT silently failed) on its first JIT call -- "the JIT script doesn't run when attaching to an app." Thread the same JIT-script callback the normal debug path uses through the hold flow: - HomeView.startKeepAlive resolves the app's preferred script (gated on hasTXM, exactly like startJITInBackground) and passes it down. - BackgroundAliveManager.start and JITEnableContext.keepAppAlive forward it to holdDebugSession. - holdDebugSession, when a script is present, runs it (the script does its own vAttach + JIT enable + continue and services the app's JIT syscalls for as long as the app uses JIT, keeping the app alive under the debugger). When absent it keeps the previous attach-and-hold behavior for older devices where CS_DEBUGGED alone enables JIT. The normal (non-hold) JIT path is unchanged. Known limitation: for an app whose script runs continuously, the hold ends when the app is closed (matching the normal flow, which likewise cannot interrupt a running script); the Stop button fully applies to the no-script hold path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DNqCsHShA4yNqgZ6ZZQy3
Honor "Hidden" points of interest for bus stops and declutter at low zoom, fix JIT scripts
…land Live Activity Keep-alive for any app: - On TXM devices (iOS 26+), keep-alive now falls back to the bundled universal JIT script for apps with no assigned/name-matched script. Previously only known apps got a script callback, so a generic app's JIT brk syscalls went unserviced and the app hung / was killed in the background — the reason keep-alive "didn't work for any app". The fallback is read straight from the app bundle so the current, cancellation-aware script is always used. - Stopping a hold now reliably stops the script loop: the hold and its JIT script share one cancellation token, exposed to JS as should_continue() and honored by send_command(). universal.js checks should_continue() each iteration. On stop we interrupt + detach so the held app keeps running instead of being torn down. The cancellation flag is only read from the script's own thread, so there is no unsafe concurrent access to the (non-thread-safe) debug proxy. Banner from the "Other" tab: - Selecting an app in the "Other" (launch) tab while keep-alive is enabled now holds it in the background and shows the banner, instead of silently launching without a debugger. The launch rows show a green "Hold" action and a footer note in this mode. Dynamic Island / Live Activity: - New StikDebugWidgets widget-extension target hosting a keep-alive Live Activity, with compact/expanded/minimal Dynamic Island presentations and a Lock Screen view. Shared KeepAliveActivityAttributes lives in a StikDebugShared group used by both the app and the extension. - BackgroundAliveManager starts the activity when a hold begins and ends it when the hold stops; stale activities from a previous run are cleared at launch. - Added NSSupportsLiveActivities to the app Info.plist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EpCxAimrBo9RA8v6BxSEXT
Drops the StikDebugWidgets extension target, the shared ActivityAttributes, and the KeepAliveLiveActivity controller — no Dynamic Island / Lock Screen presentation. The banner stays as the existing in-app overlay in HomeView, now also shown when holding an app from the "Other" tab. project.pbxproj, Info.plist, and AppBootstrapper.swift are back to their pre-widget state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EpCxAimrBo9RA8v6BxSEXT
…n830n2 Fix experimental keep-alive, wire it into "Other" tab
Keep-alive ran the universal JIT breakpoint-handler script against every app on TXM devices. That script is meant for apps that use the brk #0xf00d JIT protocol (emulators). Against a large, multi-threaded, non-JIT app like Roblox it chases every signal the app raises — logging verbosely each iteration — which: - grows RunJSViewModel.logs (a @published array rendered in a SwiftUI List) without bound until StikDebug runs out of memory and crashes, and - mishandles the app's many threads (single-thread vCont), wedging it. Two fixes: - keepAliveScript now returns a script only for user-assigned or name-matched (known JIT) apps. Everything else is held with a plain debugger attach, which keeps the app alive in the background without interfering with its threads or signals. This restores the proven plain-hold path for apps like Roblox. - appendLog caps the log buffer (with amortized trimming) so a long-running or spinning hold script can never exhaust memory. The user can still stop such a hold via should_continue(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EpCxAimrBo9RA8v6BxSEXT
…n830n2 Fix keep-alive crash on non-JIT apps (e.g. Roblox)
The no-script hold sent a single raw "continue" and then only polled a flag —
it never read the debug proxy's stop replies. A debugged process stops on every
signal/exception, so on the first stop the app stayed halted, couldn't service
its run loop, and iOS's watchdog killed it. For a large app like Roblox this
happened almost immediately, which is why keep-alive "ended instantly and the
app crashed" (and why the plain hold never really worked for any app).
Replace the fire-and-forget continue+poll with an active continue-service loop:
after attaching, continue on every stop and forward real signals (C<sig>) so the
app behaves exactly as if it were not being debugged, swallowing only the
debugger artifacts (attach SIGSTOP, SIGTRAP traps) with a plain continue. The
loop ends cleanly when the app exits (W/X reply), when the connection drops, or
when the user stops the hold (then interrupt + detach so the app keeps running).
This uses the same blocking send_command("c") mechanism the JIT script already
relies on through idle periods, so it is single-threaded and safe (no concurrent
access to the non-thread-safe debug proxy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EpCxAimrBo9RA8v6BxSEXT
…n830n2 Keep held apps alive by actively servicing the debug connection
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.
Summary
Four related improvements developed and tested on a fork:
cellular via the LocalDevVPN tunnel, but nothing detects or recovers from a
Wi-Fi↔cellular handoff, so the tunnel silently drops.
switching away from StikDebug, because the background execution assertion
isn't being renewed and the keep-alive helpers don't stay active for the
duration of a session regardless of user toggles.
with stop dwelling), bus stop markers, map style options, and a performance
fix so long routes (100+ km) don't stall route preparation.
debugger attached to a launched app so iOS doesn't suspend it in the
background (same mechanism Xcode relies on). Off by default; flagged as
experimental in Settings.
Changes
Cellular / network path monitoring
NetworkPathMonitor, wrappingNWPathMonitorto report reachability,interface type (Wi-Fi/cellular/VPN), and post a notification on change.
TunnelManagerretries the tunnel connection (3 attempts, short backoff)before failing, and reconnects automatically after a debounced network-path
change if the tunnel is down.
assuming Wi-Fi and mention cellular explicitly.
Background keep-alive
BackgroundAudioManager/BackgroundLocationManagergain aforce:parameter on
requestStart/requestStopso an active debug session can keepthem running independent of the user's Settings toggles, released when the
session ends.
(~-80 dBFS) instead of pure digital silence, since some iOS versions reclaim
a fully-silent playback session as idle.
DebugKeepAliveLease's background task now renews itself on expiry insteadof tearing the session down, as long as the lease is still active.
Location simulator
SpeedProfile(walking/jogging/cycling/driving/bus/custom) driving routeplayback pacing; bus mode also slows for and dwells at stops along the route.
highway=bus_stoptag and the modern
public_transportschema).filtering.
aroundquery sized to route length, and replace the linear way-segment scan with a
grid-bucketed spatial index — long routes no longer take minutes of CPU to
prepare.
bounded in memory without losing playback smoothness.
Experimental hold-alive mode
JITEnableContext.keepAppAlive: launches an app, attaches the debugger,lifts its memory limit, and holds the connection open (continue → poll
cancellation → interrupt → detach) so the app isn't suspended in the
background. Also enables JIT as a side effect of the attach.
BackgroundAliveManagerto own a single active hold and coordinate itwith
DebugKeepAliveLease; add a Settings toggle and a HomeView banner witha Stop action. Off by default, explicitly labeled experimental — cannot
revive an app iOS has already killed, and increases battery use.
Related Issues
None filed — these were built independently; happy to open issues first if
preferred before review.
Testing
Screenshots