Live Relay: live GPS sharing (debug-flow build)#778
Merged
Conversation
Client side of odin-core PR #1572 (Live Relay): an ephemeral, app-agnostic live-data primitive. This wires the live-GPS use case into the existing in-conversation "share location" action, with send/receive logging so we can confirm the data flows end to end before building real UX. - homebase-api: LiveRelayContract (fixed channel key + LiveLocationPoint codec), LiveRelayProvider (POST /api/v2/live-relay), liveRelay notification type + LiveRelayReceivedNotification DTO, dispatch case emitting BackendEvent.LiveRelayReceived. All logged under tag "LiveRelay". - homebase-chat: LiveLocationShareService — relays the latest GPS fix to the conversation's participants, fired from the LocationPointStore.onPointsBuffered sink seam (background/cold-wake capable, NOT a UI Flow), throttled to >=3s, with active-share state persisted for cold-process wakes. Toggled from ConversationContent's share-location action. - homebase-core: extend onPointsBuffered to drive the sender; LiveLocationDebugLogger decodes inbound blobs and logs them; both registered + lifecycle-managed in AppModule. - Tests: LiveRelayContractTest (codec round-trip, server-payload parse, no-appId request). Wire serialization confirmed against odin-core: ClientNotificationType is serialized by camelCase name, so LiveRelay -> "liveRelay" (the 6001 C# value is never on the wire). All new code is commonMain — no platform actuals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Replace the single global active/recipients flag with a {identity, end-time-utc-ms}
roster so concurrent/overlapping live shares are correct:
- same recipient in two requests collapses to one entry with the max end-time,
- overlapping shares union their recipients (each sent to once per tick),
- a recipient drops off automatically once their window expires (no manual stop),
and the tracker we started is stopped once the roster fully empties.
End-times are sender-side only — no wire change; the relay stays ephemeral.
- homebase-api: LiveShareRoster (pure merge/live helpers) + TimedRecipient.
- homebase-chat: LiveLocationShareService uses the roster; default 1h window for
the debug toggle.
- Tests: LiveShareRosterTest (max-end-time on duplicate, union, expiry pruning).
- docs/live-relay-live-gps-debug-plan.md: the implementation plan (titled with
PR #778), including the roster model and background-send rationale.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Correct the roster model: a recipient added twice now produces two distinct
{identity, end-time} entries (one per share action) instead of collapsing to a
single max-end-time entry. Keeping entries distinct lets a future UX store an
entry's end-time on its chat bubble ("share for 15 min") and remove exactly that
entry. Deduplication to unique identities happens only at send time
(LiveShareRoster.liveRecipientIds) — the same coordinate is never sent to the
same identity twice.
- LiveShareRoster.merge -> add (append, prune expired, no collapse) +
liveRecipientIds (unique live identities for fan-out).
- LiveLocationShareService sends to liveRecipientIds.
- Tests + plan doc updated for the keep-entries / dedup-on-send semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
…e map Replace LiveLocationDebugLogger with LiveLocationReceiveStore: an in-memory StateFlow<Map<OdinId, LivePosition>> of each sender's last known position + server receipt time, fed from BackendEvent.LiveRelayReceived (last-value-wins), cleared on logout. This is the data source a future map/dashboard binds to; it still logs RECV-DECODED. Deliberately NOT persisted: the server retains each sender's last point and auto-flushes on (re)connect/foreground, so the map rehydrates from the server on a cold start. Persisting would duplicate that and risk rendering positions staler than the server's ~5min TTL. (Asymmetry with the send roster, which IS persisted because it's the user's own intent.) Plan doc updated with a hydration note + the in-memory-only rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Fix: opening the app no longer stops your own share. reset() (run from onPostAuthenticated) now RE-SEEDS the roster from the current identity's DB and re-arms GPS if a still-live share needs it — instead of clearing the share. So a 2-week share keeps going regardless of cold start, manual reopen, or background wake. A share only ends on full expiry, an explicit stop(), or logout (keyValue DB wiped -> next re-seed reads empty, and a tracker we started is stopped). - reconcileTracker() centralizes tracker (re)arm/stop against the live roster; (re)starts unconditionally on a live share since a cold-started process has the tracker down even when the persisted startedTrackerByUs flag is true. - start()/stop()/prune now go through reconcileTracker(); removed ensureTrackerStarted. - Plan doc: lifecycle section rewritten; dropped the "reopen clears share" item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
… (incl iOS) Fold tracker ownership into LocationTrackingCoordinator (#2) and get the process-start re-arm (#1) for free. LiveLocationShareService no longer touches the tracker. The coordinator is the single arbiter of tracker start/stop/mode and runs GPS when wantsGps = trackingEnabled || liveShareActive(). The share service only declares its need: - hasLiveShare() -> read by the coordinator's new `liveShareActive` predicate, - onLiveShareChanged() -> pokes coordinator.refreshGpsHold() on start/stop/expiry. Wins: - Single owner, no contention between a share and the master switch; the share ending never stops capture that tracking still wants. - Correct mode for free — coordinator applies Foreground/Background by app state instead of the share hard-coding Foreground. - Reliable re-arm across a full process kill: coordinator.onProcessStart() runs on every launch (Android MainApplication; iOS initializeApp, incl. the significant-location-change relaunch) and re-arms whenever wantsGps(). This is the iOS-termination reliability fix (#1). Also: setTrackingEnabled(false) keeps GPS if a live share still needs it; drop the now-unused startedTrackerByUs/reconcileTracker and the service's scope param. Plan doc updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Make the plan doc match the shipped code after the coordinator-ownership and roster refactors: - LiveShareRoster helpers are add/live/liveRecipientIds (not "merge"); auto-expiry drops the GPS hold via the coordinator (the service no longer owns the tracker). - Name the "master switch" as LocationPreferences.trackingEnabled. - Note onLocationClick keeps the existing static-location preview AND toggles the live share. - Receive store is reset/re-subscribed on each login bootstrap (onPostAuthenticated), not "on logout". - "All code added or changed is in common source sets." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
…ation The pipeline was validated end-to-end on real hardware (frodo Android -> sam Desktop): SEND -> 204 on every captured GPS point, auto-expiry confirmed, and on the receiver RECV -> RECV-DECODED with ~400ms end-to-end latency and a clean blob decode. Results recorded in the plan doc's verification section. Remove the temporary trigger that was wired into the chat "share location" action for testing (ConversationContent now matches main again), so the half-built feature can't be reached by users before its real UX exists. The plumbing stays fully wired in DI; nothing calls LiveLocationShareService.start() yet — added a "How to activate live sharing" note to the plan doc (start/stop/isActive) for the follow-up UX work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Contributor
Author
✅ Real-device end-to-end verification (2026-06-20) — both halves confirmedValidated with a temporary chat trigger (since removed), frodo (Android, real device) → sam (Desktop App), already connected. Logs filtered on tag Send (frodo, Android)
Receive (sam, Desktop)
Before merge
|
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
Client side of odin-core PR #1572 (Live Relay — ephemeral, app-agnostic live-data streaming among connected identities). Wires the live-GPS use case into the existing in-conversation "share location" action, with send/receive logging so we can confirm the data flows end to end before building real UX (tag
LiveRelay).📄 Implementation plan:
docs/live-relay-live-gps-debug-plan.mdWhat's here
LiveRelayContract(fixed channel key +LiveLocationPointcodec),LiveShareRoster(recipient roster, see below),LiveRelayProvider(POST /api/v2/live-relay),liveRelaynotification type +LiveRelayReceivedNotification, dispatch case emittingBackendEvent.LiveRelayReceived.LiveLocationShareService— relays the latest GPS fix to live recipients, fired from theLocationPointStore.onPointsBufferedsink seam (background / cold-wake capable, not a UI Flow), throttled to ≥3s, roster persisted for cold-process wakes. Toggled fromConversationContent.onPointsBufferedto drive the sender;LiveLocationDebugLoggerdecodes inbound blobs and logs them; both registered + lifecycle-managed inAppModule.LiveRelayContractTest+LiveShareRosterTest.Recipient roster —
{identity, end-time-utc-ms}pairsRecipients are kept as
TimedRecipient(odinId, endTimeMs)rather than a flat on/off list, so concurrent shares are correct:End-times are sender-side bookkeeping only — no wire change; the relay stays ephemeral/last-value-wins.
Notes
ClientNotificationTypeis serialized by camelCase name, soLiveRelay→"liveRelay"(the6001C# value is never on the wire).commonMain— no platform actuals. Verified locally: JVM + Android + wasmJs compile,homebase-api:jvmTestgreen. CI proves the iOS target (linkDebugFrameworkIosArm64) + sim tests.homebase.log | grep LiveRelay).🤖 Generated with Claude Code