Commit Graph
13 Commits
Author SHA1 Message Date
jackandGitHub 820c933958 Harden PTT audio and the 1.7.1 release (#1423)
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage.

Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
2026-07-10 14:04:09 +02:00
dd6b624cae Quality pass on the 1.7.0 batch: fix confirmed bugs, bump to 1.7.1 (#1418)
* Quality pass on the 1.7.0 batch: fix confirmed bugs, bump to 1.7.1

Post-merge review of PRs #1400–#1417 (push-to-talk, mesh bridging, DM
store-and-forward, empty-mesh liveliness, geo-notes). Fixes the confirmed,
well-scoped findings; deeper architectural/security items are tracked
separately.

- PTT hot-mic leak: releasing the mic during VoiceCaptureSession.start()'s
  150ms retry pause left the mic live and streaming for up to 120s, because
  cancel() no-op'd once `completed` was set. Bail after the sleep if the hold
  was released, and make cancel() always tear down a late-started capture.
- Bridge courier depositDrop reported success and burned the dedup slot before
  the drop was actually published (evicted/compose-fail = lying 📦 "carried"
  with no retry). Only consume publishedDropKeys on durable accept; add
  BoundedIDSet.remove() to release evicted/failed slots (uses the dead dedupKey).
- Blocked senders resurfaced via archived "heard here earlier" echoes, the one
  path that bypassed the live block filter — filter at seed time.
- A late optimistic .sent clobbered the router's .carried state; extend
  ConversationStore.shouldSkipStatusUpdate to a full precedence guard
  (sending < sent < carried < delivered < read).
- Read receipts were permanently burned when the router dropped them (marked
  sent then dropped). sendReadReceipt/routeReadReceipt now return Bool; only
  record as sent on a successful route, else retry on the next read scan.
- MessageRouter.cleanupExpiredMessages() had no production caller, so DMs to a
  peer that never reconnects sat on .sending until relaunch — run it in the
  120s bridge sweep.
- Sightings tally now rolls over at midnight while idle; wave notification
  action localized across all 29 locales; bridged anon#tag uses suffix(4) like
  everything else; makeThrowawayIdentity delegates to NostrIdentity.generate();
  .swiftlint.yml excludes .claude worktrees.
- Add regression tests: carried→sent no-downgrade, carried→delivered upgrade,
  evicted pending drop stays retryable.
- Bump MARKETING_VERSION to 1.7.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix CI: adjust delivery-status benchmark and drop now-dead addSystemMessage

The stricter no-downgrade guard (delivered/carried never regress to sent) broke
two things the earlier commit didn't catch locally (perf tests are skipped in
the default run, and Periphery runs only in CI):

- PerformanceBaselineTests delivery benchmarks alternated sent <-> delivered
  assuming both directions apply; the delivered -> sent half is now correctly
  skipped, so the pass measured 0 updates. Alternate two delivered timestamps
  instead — every update is real, no downgrade.
- Routing the geoDM "not in a location channel" error into the thread removed
  the only caller of ChatPrivateConversationContext.addSystemMessage, leaving
  it (and its mock) dead per Periphery. Drop the protocol requirement, the mock
  impl, and the now-vacuous systemMessages.isEmpty assertions (the invariant is
  compile-time enforced: the context can no longer emit a public system line).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address review findings: read-receipt dedup, carried-vs-sending, block-time echo purge

- Read receipts: claim the receipt in sentReadReceipts synchronously
  before spawning the routing task (chat open runs two read scans in one
  MainActor stretch, so the async insert let every unread message route
  twice), and release the claim when the route fails so the
  retry-on-failed-route behavior is preserved.

- Delivery status: extend the no-downgrade guard so the `.sending`
  stamp a pre-handshake resend emits can no longer clobber
  carried/delivered/read (the 📦 indicator survived `.sent` but not
  `.sending`).

- Archived echoes: blocking a peer now purges their carried public
  messages from the gossip archive at block time (UnifiedPeerService
  and /block), while the fingerprint-to-peerID mapping is still known —
  the seed-time filter can't resolve offline non-favorite strangers and
  stays only as defense-in-depth. New Transport hook (default no-op) +
  GossipSyncManager.removePublicMessages with immediate persist.

- Bridge courier: an envelope that can't encode within the drop size
  caps fails identically on every attempt; consume the dedup slot so
  the 120s retry sweep stops re-running Noise sealing on it.

- MeshSightingsTracker: cache the day-key DateFormatter instead of
  building one per call.

Tests: double-markAsRead dedup + failed-route retry, carried→sending
no-downgrade matrix, block-time purge (manager + service wiring),
oversize-drop slot consumption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Also skip the sent→sending downgrade in the delivery-status guard

Codex review follow-up: sendPrivateMessage without an established Noise
session emits `.sending` asynchronously, so it can land after the
message already reached `.sent` and visibly walk "Sent" back to
"Sending...". Treat `.sending` as weaker than `.sent` too — the status
was already truthful. `.failed` → `.sending` stays allowed so a retry
after a real failure remains visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Retain Codable properties in Periphery scan (same fix as #1421)

The noiseKey assign-only false positive fired persistently on this branch
(twice, including a rerun) despite the baselined USR. Byte-identical to the
fix on fix/announce-replay-link-steal so the branches merge cleanly in
either order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:44:02 +02:00
75da63c9d7 Fix favorites end-to-end: peer-list duplicates, Nostr sync, /fav key corruption (v1.5.4) (#1367)
* Friend-courier store-and-forward: mutual favorites carry sealed messages to offline peers

When a private message has no reachable transport, the router now seals it
to the recipient's Noise static key (new one-way Noise X pattern) and hands
the envelope to up to three connected mutual favorites. Couriers store the
opaque ciphertext under strict quotas (20 total, 5 per depositor, 16 KiB,
24 h) and hand it over when the recipient's announce matches a rotating
HMAC recipient tag; the recipient opens it and the message flows through
the normal private-message pipeline, so dedup and delivery acks just work.

- CourierEnvelope TLV + courierEnvelope (0x04) message type in BitFoundation
- Noise X one-way pattern reusing the existing handshake machinery,
  domain-separated by a courier prologue; sender identity authenticated
  via the ss DH (no forward secrecy - documented tradeoff)
- CourierStore with eviction, file persistence, and panic-wipe integration
- Rotating recipient tags (HMAC over epoch day) so carried envelopes don't
  correlate for observers who don't already know the recipient's key
- New "carried" delivery status with figure.walk glyph; header indicator
  while carrying mail for others
- Three-node end-to-end test ferrying packets through real BLEService
  instances, plus codec/crypto/store/router suites (986 tests green)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix courier handoff verification and directed sends

* Authenticate courier deposits by ingress peer

* Gate courier handover on direct announces and isolate store test

Envelopes are removed from the courier store optimistically, so releasing
them on a relayed (multi-hop) announce risks losing carried mail to a
speculative flood that never reaches the recipient. Handover now also
requires the announce to have arrived directly (full TTL), i.e. an actual
encounter with a live link; regression test builds a relayed copy of a
genuinely signed announce (TTL is excluded from announce signatures).

Also make CourierStore's on-disk location injectable so the persistence
test round-trips through a temp directory instead of wiping the real
Application Support store, and reattach BLEAnnounceHandler's doc comment
to the class it describes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Use Xcode-bundled Swift in CI instead of a standalone toolchain

The unpinned setup-swift action installs Swift 6.1, which refuses the
SDK on runner images that have rolled to Xcode 26.5 ("this SDK is not
supported by the compiler"). Jobs passed or failed depending on which
image they landed on. The Xcode-bundled toolchain always matches the
image's SDK, and matches local development. Cache keys now include the
toolchain version so artifacts from one compiler are never restored
into builds with another.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop couriered mail from blocked senders at envelope open

The UI-layer block check (isPeerBlocked in the transport event
coordinator) resolves a fingerprint from the live session or peer list,
but a couriered message arrives precisely when its sender is absent —
no session, no registry entry — so the check failed open and a blocked
identity's mail was delivered anyway. Gate in openCourierEnvelope,
where the sealed sender's full static key is in hand.

End-to-end test ferries a full deposit→carry→handover round and
verifies the envelope from a blocked sender never reaches the delegate
(confirmed failing without the gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix favorites end-to-end: peer-list dedup, Nostr sync, /fav key corruption

- UnifiedPeerService: dedup offline favorites against mesh peers by noise
  key. Phase 2 compared a 64-hex noise-key PeerID against 16-hex mesh IDs
  (never equal), leaving only a nickname+isConnected heuristic — a mutual
  favorite that was reachable-but-not-connected or renamed rendered twice,
  and a same-nick stranger could suppress a favorite entirely.
- Nostr inbound: intercept [FAVORITED]/[UNFAVORITED] markers in the live
  PM handler so they update theyFavoritedUs instead of rendering as chat
  text; mutual favorites can now form over Nostr. Delete the dead
  favorite-aware PM variant and ChatNostrCoordinator.handleFavoriteNotification
  (unwired, parsed a stale FAVORITE:TRUE|… format no sender emits).
- NostrTransport.isPeerReachable: match short form regardless of incoming
  ID width — toggling an offline favorite (addressed by 64-hex noise key)
  was silently dropped with no reachable transport.
- BLEService.sendPrivateMessage: normalize recipient to the short ID like
  sendFilePrivate, so a 64-hex target hits the existing Noise session
  instead of initiating a handshake with a 32-byte wire recipient ID.
- /fav, /unfav: stop writing Data(hexString: peerID.id) — the 8-byte
  routing ID for mesh peers — into the favorites store as a "noise key",
  and stop double-sending the favorite notification; delegate to
  toggleFavorite with a proper state check.
- FavoritesPersistenceService.updatePeerFavoritedUs: keep the stored
  nickname when the caller passes the "Unknown" placeholder.
- Bump marketing version to 1.5.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Route DMs to mutual favorites via Nostr when a mesh-keyed peer goes offline

Field-tested on device: with a DM window opened while the peer was on
mesh (conversation keyed by the short 16-hex ID), walking out of range
and sending failed instantly with "peer not reachable" even though the
header showed the peer as Nostr-reachable (mutual favorite, npub known).

sendPrivateMessage derived the favorites key as Data(hexString:
peerID.id) — for a short mesh ID that is the 8-byte routing ID, never
the noise key — so the mutual-favorite/Nostr-key checks always came up
empty and the send failed before reaching MessageRouter. Conversations
keyed by the full 64-hex noise-key ID (opened from the offline favorite
row) were unaffected, which is why later tests appeared to work.

Resolve the noise key properly (peerID.noiseKey, then the unified peer
row, then the favorites store by derived short ID) and add a regression
test for the mesh-keyed-peer-goes-offline case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Label Nostr DMs from favorites with their stored nickname

Field-tested: a DM delivered over the Nostr fallback rendered as
"anon#678e" instead of the sender's name. The inbound handler named the
sender via displayNameForNostrPubkey, which only knows geohash-scoped
names — even though the pipeline had already resolved the sender's
noise key (the conversation is keyed by it).

When the conversation key carries a noise key, prefer the favorite's
stored nickname; geohash DMs (nostr_ keys) keep the anon geo name. This
also stops an inbound Nostr [FAVORITED] from overwriting the stored
nickname with the anon fallback, since the same name feeds
updatePeerFavoritedUs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix courier path for offline favorites addressed by noise-key IDs

Two Codex review findings, both the same ID-width confusion this PR
targets, in the courier flow:

- CourierDirectory.favoritesBacked resolved recipients only via
  getFavoriteStatus(forPeerID:), which requires a short 16-hex ID —
  offline favorites are addressed by the full 64-hex noise-key ID, so
  attemptCourierDeposit silently bailed for exactly the peers couriers
  exist to serve. The 64-hex ID now yields its own key directly.
- openCourierEnvelope emitted the derived short mesh ID even when the
  sender has no live mesh identity, landing couriered mail in an
  unresolvable short-ID thread labeled "Unknown". Absent senders now
  emit the full noise-key ID so the message joins the stable favorite
  conversation; present senders keep the live short-ID thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:17:21 +02:00
jackandClaude Fable 5 ed86ed1065 Surface router drops as failed status; bound relay pending subscriptions; jitter backoff
MessageRouter outbox drops (attempt cap, TTL expiry in flush and
cleanup, per-peer overflow eviction) now invoke onMessageDropped, wired
to mark the message .failed in the ConversationStore - guarded so a
late failure never downgrades an already delivered/read status.

NostrRelayManager pending subscriptions gain a per-relay cap (64,
oldest-by-sequence eviction; durable intent still replays from
subscriptionRequestState) and a 10-minute age sweep on the existing
connect path. Reconnect backoff gets injectable +/-20% jitter so
recovering relays don't thundering-herd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:05:44 +02:00
jackandClaude Fable 5 7fb1f4a219 Route delivery status through the store; delete the location index
ConversationStore maintains an exact messageID -> Set<ConversationID>
map at every mutation point (append/upsert/remove/migrate/trim/clear),
so delivery updates are ID-only lookups that fan out to mirrored
ephemeral/stable copies. ChatDeliveryCoordinator shrinks 327 -> 119
lines: the positional location index, its growth-detection/rebuild
machinery, and the duplicate no-downgrade check are deleted - the rule
now lives in exactly one place. The middle-insertion regression tests
are rewritten against the store since stale positional locations are
structurally impossible now.

delivery updates: 38k -> 262k/s (~6.9x); ingest pipelines unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:26:00 +02:00
jackandClaude Fable 5 99d1d1dccd Cut public message path over to ConversationStore; delete PublicTimelineStore
Mesh and geohash timelines are now store conversations. All public
mutation sites flow through store intents; PublicMessagePipeline keeps
its 80ms UI batching but commits batches via store appends with each
buffered entry carrying its destination conversation (a mid-batch
channel switch now flushes instead of dropping the buffer).
ChatViewModel.messages becomes a cached get-only view of the active
conversation, invalidated through the change subject. The mesh
late-insert threshold is consciously removed: it only ever ordered the
non-rendered messages copy, so strict timestamp insertion makes the
working set agree with rendered order. PublicTimelineStore and the
per-message full-array legacy sync are deleted; the coalescing bridge
mirrors public conversations for the remaining legacy readers.

pipeline.publicIngest: 6.6k -> 9.5k msg/s (+45%); private steady;
store.append 237k/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:03:07 +02:00
jackandClaude Fable 5 879d8cba12 Cut private message path over to ConversationStore
All private-message mutations now flow through store intents:
coordinators, PrivateChatManager (its @Published dicts deleted - now
read-only views over the store), outbound sends, delivery status, and
chat migration. The O(1) store dedup replaces the full-scan duplicate
check; insertion order is maintained by the store so sanitizeChat's
re-sort is a documented no-op. Both bootstrapper Combine bridges and
the Task.yield store synchronization are deleted.

ChatViewModel.privateChats/unreadPrivateMessages become get-only derived
views (measured: naive rebuild equals a change-invalidated cache within
noise, so the simpler form stays). Feature models still read the legacy
store, fed by a coalescing LegacyConversationStoreBridge (one mirror
per burst, marked for step-5 deletion).

pipeline.privateIngest: 9.6k -> 14.7k msg/s (+53%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:19:11 +02:00
jackandClaude Fable 5 707b22878d Convert shared coordinator state to owner-side intent operations
nostrKeyMapping, sentGeoDeliveryAcks/sentReadReceipts dedupe,
isBatchingPublic, geo subscription lifecycle, and private-chat selection
hand-off now mutate through single intent operations on ChatViewModel,
with backing storage locked down via private(set) so the single-writer
property is compiler-enforced. Context protocols downgrade to read-only
access where reads remain. 8 new contract tests.

Known remaining writers outside the protocols: sentReadReceipts is
passed inout to PrivateChatManager.syncReadReceiptsForSentMessages and
un-marked by ChatTransportEventCoordinator on disconnect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 21:26:21 +02:00
jackandClaude Fable 5 19b28cf49d Rebuild message location index on non-append growth
The incremental index refresh assumed growth meant appended messages,
but PublicMessagePipeline inserts out-of-order arrivals by timestamp:
the count grows while the tail ID stays put, so the inserted message
never entered the index and later delivery updates for it silently
no-op'd (and, with retain-until-ack routing, left it queued for
resend). Detect non-append growth by checking the previously indexed
tail kept its position, and rebuild when it hasn't. Same check on the
per-peer private chat arrays, which re-sort by timestamp.

Found by Codex review on #1331.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:32:36 +01:00
jackandClaude Fable 5 3a995e20b6 Extract BLE packet handlers and migrate coordinators to narrow contexts
BLEService's per-packet-type orchestration moves into owned, tested
components (BLEAnnounceHandler, BLEPublicMessageHandler,
BLENoisePacketHandler, BLEFileTransferHandler, BLEFragmentHandler),
each taking an environment struct of closures so every queue hop stays
in BLEService and the handlers are synchronously testable. Behavior is
preserved verbatim, including Noise session recovery on decrypt failure
and single-block UI event ordering. handleLeave/handleRequestSync stay
in place as already-thin delegations. BLEService drops to 3393 lines.

Four coordinators (delivery, private conversation, Nostr, public
conversation) drop their unowned/weak ChatViewModel back-references for
narrow @MainActor context protocols, with ChatViewModel conformances as
single shared witnesses for overlapping members. Their true coupling is
now an explicit, reviewable surface, and each gains a mock-context test
suite covering flows previously testable only through the full view
model. Delivery/read acks now also clear the router's retained-send
outbox via the delivery context.

New LargeTopologyTests exercise production-shaped meshes with the
in-memory harness: an 8-peer relay chain with per-hop TTL decay, a
14-peer cyclic mesh with exactly-once delivery, partition/heal, and
topology churn.

App-layer runtime/model files updated alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:22:52 +01:00
764f016d17 [codex] Refactor app runtime and ownership architecture (#1104)
* Refactor app runtime and view model architecture

* Move app ownership into stores and coordinators

* Fix smoke test environment injection

* Stabilize fragmentation package tests

* Fix coordinator build warnings

* Clean up chat view model warnings

* Fix Nostr relay startup coalescing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2026-05-30 14:13:26 +02:00
IslamandGitHub 4cfcefcda6 BitFoundation module to centralize shared components (#1089)
* Run local packages’ tests as well on CI

* BitFoundation module to centralize shared components
2026-04-14 14:10:03 -05:00
806c420313 Add comprehensive test coverage for ChatViewModel and BLEService (#962)
* Add comprehensive test coverage for ChatViewModel and BLEService

This commit adds 14 new tests to improve the safety net for future
refactoring of ChatViewModel and BLEService:

New test files:
- ChatViewModelTorTests: Tor lifecycle notification handlers (8 tests)
- ChatViewModelDeliveryStatusTests: Delivery status state machine (6 tests)
- ChatViewModelRefactoringTests: Command routing and message handling (4 tests)
- BLEServiceCoreTests: Packet deduplication and stale broadcast filtering (2 tests)
- PublicMessagePipelineTests: Message ordering and deduplication (4 tests)
- MessageRouterTests: Transport selection and outbox behavior (4 tests)
- PrivateChatManagerTests: Chat selection and read receipts (2 tests)
- UnifiedPeerServiceTests: Fingerprint resolution and blocking (2 tests)
- RelayControllerTests: TTL, handshake, and fragment relay logic (4 tests)

Modified files:
- MockTransport: Added peer snapshot publishing on connect/disconnect
- TestHelpers: Added waitUntil polling helper for async tests
- MockIdentityManager: Extended for new test scenarios

Total: 454 tests across 64 suites (was 440)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix flaky test by using waitUntil instead of fixed sleep

Replace fixed 200ms sleep with waitUntil helper for more reliable
async assertion in routing tests. This prevents timing issues on CI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 16:02:13 -10:00