* 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>
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).
Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.
Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).
New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.
Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Production builds previously still emitted info/warning/error entries
via os_log (content private-redacted, but entries, categories, and
timing metadata were visible, and message strings were constructed).
For a privacy-first app the right posture is silence: every SecureLogger
wrapper and both cores are now gated behind #if DEBUG, so release
builds construct no log strings and emit nothing. Debug builds are
unchanged (public formatting, level threshold via BITCHAT_LOG_LEVEL).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SecureLogger's public wrappers evaluated their message autoclosure before
the level check inside log(), so every filtered debug message across the
codebase still paid for string interpolation - hundreds of call sites on
the per-packet/per-event hot paths. The wrappers now guard the level
first, and debug() compiles out of release builds entirely. Regression
tests verify filtered messages are never constructed.
The two heaviest per-event debug logs (NostrRelayManager inbound events,
ChatNostrCoordinator geo events) are now sampled every 100th with a
running count, so debug-enabled dev builds stop emitting hundreds of
lines per minute in busy geohashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>