Skip to content

Live Relay: live GPS sharing (debug-flow build)#778

Merged
Seifert69 merged 8 commits into
mainfrom
live-relay-location-debug
Jun 21, 2026
Merged

Live Relay: live GPS sharing (debug-flow build)#778
Seifert69 merged 8 commits into
mainfrom
live-relay-location-debug

Conversation

@Seifert69

@Seifert69 Seifert69 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

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.md

What's here

  • homebase-api: LiveRelayContract (fixed channel key + LiveLocationPoint codec), LiveShareRoster (recipient roster, see below), LiveRelayProvider (POST /api/v2/live-relay), liveRelay notification type + LiveRelayReceivedNotification, dispatch case emitting BackendEvent.LiveRelayReceived.
  • homebase-chat: LiveLocationShareService — relays the latest GPS fix to live recipients, fired from the LocationPointStore.onPointsBuffered sink seam (background / cold-wake capable, not a UI Flow), throttled to ≥3s, roster persisted for cold-process wakes. Toggled from ConversationContent.
  • homebase-core: extend onPointsBuffered to drive the sender; LiveLocationDebugLogger decodes inbound blobs and logs them; both registered + lifecycle-managed in AppModule.
  • Tests: LiveRelayContractTest + LiveShareRosterTest.

Recipient roster — {identity, end-time-utc-ms} pairs

Recipients are kept as TimedRecipient(odinId, endTimeMs) rather than a flat on/off list, so concurrent shares are correct:

  • same recipient in two requests → one entry, latest end-time wins;
  • overlapping shares → union of recipients (each sent to once per tick);
  • auto-expiry → a recipient drops off once their window passes; no manual stop needed.

End-times are sender-side bookkeeping only — no wire change; the relay stays ephemeral/last-value-wins.

Notes

  • 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. Verified locally: JVM + Android + wasmJs compile, homebase-api:jvmTest green. CI proves the iOS target (linkDebugFrameworkIosArm64) + sim tests.
  • Depends on odin-core #1572 being deployed for the live manual test (two connected identities, watch homebase.log | grep LiveRelay).

🤖 Generated with Claude Code

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
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

JVM Test Results

2 357 tests  +11   2 352 ✅ +11   1m 49s ⏱️ -1s
  287 suites + 2       5 💤 ± 0 
  287 files   + 2       0 ❌ ± 0 

Results for commit 0685a0a. ± Comparison against base commit a2acc48.

♻️ This comment has been updated with latest results.

Seifert69 and others added 7 commits June 20, 2026 11:19
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
@Seifert69

Copy link
Copy Markdown
Contributor Author

✅ Real-device end-to-end verification (2026-06-20) — both halves confirmed

Validated with a temporary chat trigger (since removed), frodo (Android, real device) → sam (Desktop App), already connected. Logs filtered on tag LiveRelay.

Send (frodo, Android)

START +1 entries=1 uniqueLive=1 until=…
SEND ch=7a1e9c40-… n=1 bytes=172 -> 204    (one per captured GPS point, zero relay-failed)
  • Background cadence is OS-throttled as designed — sends track GPS deliveries 1:1, multi-minute gaps when the backgrounded/stationary device emitted no points (the ≥3 s gate was never the limiter).
  • Auto-expiry proven: a GPS point buffered after the share's until produced no SEND — the expired roster entry was pruned and the relay stopped.

Receive (sam, Desktop)

RECV from=frodo… ch=7a1e9c40-… bytes=172 receivedAt=…
RECV-DECODED from=frodo… lat=55.84… lon=12.57… ageMs=401 tracked=1
  • Wire serialization confirmed live — the liveRelay case fired, so the server really sends camelCase "liveRelay" and our enum member matched.
  • Blob decodes cleanly across Android→Desktop (no RECV-DECODE-FAIL); position landed in LiveLocationReceiveStore (tracked=1).
  • End-to-end latency ~400 ms (ageMs=401) across all three relay hops.

Before merge

  • The temporary chat trigger was removedConversationContent matches main again, so the half-built feature isn't user-reachable. The plumbing stays fully wired in DI; activate from code via LiveLocationShareService.start(recipients) (see the "How to activate live sharing" note in docs/live-relay-live-gps-debug-plan.md).
  • Still-untested paths for the UX build: share with the location-tracking switch OFF (exercises the coordinator's GPS re-arm incl. iOS relaunch), and flush-on-connect.

@Seifert69 Seifert69 closed this Jun 20, 2026
@Seifert69 Seifert69 reopened this Jun 20, 2026
@Seifert69 Seifert69 enabled auto-merge June 20, 2026 13:49
@Seifert69 Seifert69 disabled auto-merge June 21, 2026 06:11
@Seifert69 Seifert69 merged commit e67130c into main Jun 21, 2026
11 checks passed
@Seifert69 Seifert69 deleted the live-relay-location-debug branch June 21, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant