Repository navigation
Tags
Tags
v0.2.9: Make Signal message identity edit-stable, dedup existing rows An edit to a Signal message produced a new (conv, sender, timestamp, body)-derived message_id that didn't collide with the pre-edit row's id, so the edited version landed as a second row instead of updating in place. The v0.2.8 periodic full rescan surfaced 38 such duplicate clusters on Max's archive — the live path had stored pre-edit bodies and the importer now re-ingested post-edit bodies under different ids. Signal's own identity for a message is (sender, sent-timestamp). Drop body from the three hash inputs so the same logical message always maps to the same row: 1. internal/signallive/client.go:signalIncomingSourceID — used by live receive, sync echo dedup, and signalQuoteReplyID (via signalIncomingSourceID) so a reply to an edited message still resolves to the same target. 2. internal/importer/signal_desktop.go:signalDesktopMessageIDs — both the incoming and outgoing (`me`) variants, so incremental rescans don't diverge from the live-path ids. 3. internal/importer/signal_desktop.go:signalDesktopReplyToID — same reasoning for quoted-reply targeting. One-shot consolidation migration in db.go:migrate keeps the row with (most reactions, longest body, smallest message_id) for each (conv, sender, timestamp) Signal cluster and deletes the rest on store open. Idempotent. Tests: existing TestSignalDesktopImportFromDB verifies outgoing ReplyToID still matches the incoming message's id after the hash change (recomputed via signalDesktopMessageIDs at assert time). Full suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.8: Belt-and-suspenders auto-reconcile for Signal drift
Two defenses against the live-receive drift pattern we keep seeing:
1. Fix the incremental window anchor.
SignalDesktop.ImportFromDB defaulted its sinceMS window to
`LatestTimestamp("signal") - 5min`. When the user sends a recent
outgoing message, the latest overall timestamp jumps past incoming
messages that signal-cli missed during a restart window — the
importer's window then starts after those gaps and leaves them
uncovered. Switch the anchor to `LatestReceivedTimestamp("signal")`
so user-outgoing activity can't shrink the incoming-coverage
window. New Store method with test.
2. Periodic full rescan in serve.
Add a background goroutine that, every 30 minutes, calls the
Signal Desktop importer with SinceMS=-1 (explicit full scan) so
any drift that slipped past both the live signal-cli WebSocket
AND the 30-second incremental sync is eventually caught. First
run is delayed 2 minutes after startup to avoid piling onto a
crash-restart cycle. Wrapped in recover() + logged at debug.
Publishes conversations+messages when new rows land.
Together with the v0.2.7 WAL these form a layered defense:
- live path: write-ahead log around signal-cli output (v0.2.7)
- 30s incremental: now anchored on received-ts (v0.2.8)
- 30min full: catches everything else (v0.2.8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.7: Durable WAL for signal-cli receive so events survive crashes signal-cli ACKs a batch of messages to Signal's servers while it's streaming them to stdout — by the time our receive loop sees the bytes, the messages are consumed from the server's perspective. If OpenMessage then crashes, is SIGKILLed, or a DB write panics between reading `output` and committing the rows, those messages are gone forever. That's what produced the two missing Deric messages and the missing 👍 reaction earlier: the backend was restarted during a delivery window and the in-flight batch was lost. Fix: wrap handleReceiveOutput with a write-ahead log. 1. Before processing any line, append the entire raw batch (one JSON line per record) to <configDir>/signal-receive-wal.ndjson with fsync so the bytes hit the disk before the DB write attempts. 2. Process each line exactly as before. processReceiveLine already quarantines unprocessable lines to the recovery queue, which is a separate durable file, so per-line failures still persist. 3. On successful completion of the batch, truncate the WAL. 4. On startup, drainReceiveWAL replays any entries left over from a prior crash or forced shutdown before the receive loop starts polling again. DB writes are idempotent on source_id so replay is safe even for lines that did commit before the crash. Tests: - TestReceiveWALIsClearedAfterSuccessfulBatch — happy path, WAL is empty after a clean batch. - TestDrainReceiveWALRecoversPendingBatch — simulates a crash by writing the WAL directly, then verifies drainReceiveWAL replays the two pending payloads into the store. Recovery path (`openmessage import signal`) is still the manual recourse for older losses; this fix prevents new ones from the live path going forward. Libgm (Google Messages) uses a WebSocket with server-side redelivery, so the same WAL treatment is not needed there — send failures on that side are now surfaced via the v0.2.6 fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.6: Surface Google Messages send failures instead of silently dro… …pping Google Messages' SendMessage returns a non-SUCCESS status (FAILURE_2/3/4, UNKNOWN) when the send is rejected at the protocol layer — typically because the phone is no longer default SMS app, RCS fallback failed, or the pairing is stale. The three /api/send* handlers (text, media, and draft) returned HTTP 200 with the failure status buried in a JSON body field the frontend didn't inspect. Result: the compose box cleared, the optimistic bubble disappeared, and the user thought the message went — but nothing was ever persisted locally and nothing landed on Google's side. Confirmed in the wild: a user-reported missing SMS reply that was not in any local DB, not in any backfill from Google's server, and had no trace — silently lost. Now on !SUCCESS: 1. Record a row with Status="OUTGOING_FAILED:<reason>" so the thread shows the attempt and its failure indicator (the existing status-failed renderer already handles any status containing "FAILED"). 2. httpError 502 with the specific status reason, so the frontend's existing send-error catch block surfaces a toast and restores the compose text for retry. Previously this path returned 200. Applied at api.go:662-685 (text), 822-846 (media), and 1212-1234 (draft send). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.5: Recover Signal Desktop messages with non-standard type values The Signal Desktop importer's SQL filter rejected any message whose m.type wasn't literally 'incoming' or 'outgoing'. Two real text messages in Max's local DB had non-standard type values (likely 'story-reply' or 'text' from a story-thread migration state) and were silently dropped by the importer even though Signal Desktop stored them and displayed them fine. Expand the WHERE in signal_desktop_export.cjs to also include any row whose type is not an explicit system/notification type AND that carries a non-empty body or at least one attachment. The allow-list of rejected system types covers the known Signal Desktop notification families: keychange, verified-change, group-v*-change, profile-change, timer-notification, delivery-issue, universal-timer-notification, change-number-notification, contact-removed-notification, conversation-merge, title-transition-notification, phone-number-discovery, message-request-response-event, session-switchover, chat-session-refreshed. Verified against Max's local DB — re-running `openmessage import signal` after the fix pulled 408 previously-missed rows into messages.db, including the two messages Max reported missing from "AI at CBO". This only addresses the importer. If the live signal-cli receive path silently drops the same message types, `openmessage import signal` is now the reliable recovery command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.4: Handle common WhatsApp message types and silence control traffic Fresh installs on active group chats showed "[Unsupported message]" rows for every group-key rotation, admin protocol event, and newer interactive/template/list message type — 125 such rows in one local DB alone, clustered in active community threads. Root cause: extractMessageBody had a short switch and fell through to hasUnsupportedWhatsAppContent for anything with non-zero proto size. Split the fall-through into three buckets: 1. Silent control traffic returns empty body (no DB row): ProtocolMessage (all types — revoke and edit have their own ingestion paths), SenderKeyDistributionMessage, FastRatchetKeySenderKeyDistributionMessage, PlaceholderMessage, SecretEncryptedMessage, and MessageContextInfo-only envelopes. These are protocol plumbing that should never surface. 2. Interactive/structured messages extract user-visible text when present and fall back to typed placeholders otherwise: InteractiveMessage (body/header text), InteractiveResponseMessage, ButtonsMessage (contentText/text), ButtonsResponseMessage (selected text), ListMessage (description/title), ListResponseMessage, TemplateMessage, TemplateButtonReplyMessage, HighlyStructuredMessage. 3. Commerce/scheduling/newsletter/keep-in-chat placeholders: KeepInChatMessage, ProductMessage, OrderMessage, InvoiceMessage, RequestPhoneNumberMessage, NewsletterAdminInviteMessage (with newsletter name), ScheduledCallCreationMessage (with title), ScheduledCallEditMessage. Diagnostics: BackendManager's pipe-to-logger interpolation now marks the interpolated line as privacy: .public, so the existing info-level log "WhatsApp message fell through to [Unsupported message]" with its content_types field is visible via log show --predicate 'subsystem == "com.openmessage.app"' without needing `sudo log config --mode private_data:on`. Future unhandled types can be identified from Console in minutes instead of being opaque. Tests: new cases for ProtocolMessage, SenderKeyDistributionMessage, InteractiveMessage, ButtonsMessage, ListMessage, KeepInChatMessage, and TemplateMessage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.3: Fix launch crash on fresh install from Contacts TCC callback Fresh installs crash immediately on launch with EXC_BREAKPOINT in ContactsManager.ensureAccess. Root cause: the @MainActor-isolated continuation closure passed to CNContactStore.requestAccess is invoked by the Contacts framework on its private background queue. Swift 6's dispatch_assert_queue check (swift_task_checkIsolatedSwift) then trips because the closure's declared isolation (MainActor) doesn't match the executing queue, and SIGTRAPs. Only users whose prior install had already granted Contacts escaped this — the .authorized branch of ensureAccess returns synchronously and never hits the completion callback path. Fresh downloads from the v0.2.1/v0.2.2 DMG hit it every time. Fix mirrors the pattern from 976d34f (NotificationManager): extract the callback-bridging async/await into a `nonisolated static` helper so the closure is not MainActor-isolated. Construct CNContactStore inside the helper to avoid passing a MainActor-isolated value across the actor boundary (which Swift 6 also rejects). Reproducer: macos/tests/fresh-install-launch.sh resets the TCC grant for com.openmessage.app and launches the installed .app, checking that the process is still alive after 15s. Verified against the live v0.2.2 DMG — repro script exits 1 with the crash log path. Will pass on v0.2.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.2: Fix Signal captioned-media double-send + launch polish - Signal captioned images were being sent twice (once via /api/send-media with the caption bundled, then again via /api/send with the same text). The client guard that should have skipped the second call only checked WhatsApp. Introduce sendsCaptionedMedia covering WA and Signal; guard the follow-up /api/send on it. - Instrument matchLocalOutgoingMessage to warn when an outgoing Signal sync echo fails to dedup against the local send record (body mismatch or >15s timestamp drift), so phantom "Fetching image…" rows can be diagnosed from logs. - Launch polish: bump CFBundleShortVersionString 1.0.0→0.2.2 and CFBundleVersion 10→11; use buildVersion (not "0.1.0") in the MCP server handshake; bump .claude-plugin/plugin.json to 0.2.2; fix linkpreview User-Agent to point to openmessage.ai. - Enrich demo seed (Jordan Signal thread) to match the launch hero. - Tests: e2e regression asserting a captioned Signal send produces exactly 1 /api/send-media and 0 /api/send; Go unit test covering four dedup outcomes (hit, body mismatch, ts drift, no candidate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenMessage v0.2.1 Bug fixes and signing infrastructure. Fixes: - 'Backend exited unexpectedly' on macOS (cleaned up zombie processes, fixed missing entitlements file that was breaking the signing step) - New-message dialog: contact name autocomplete (search both contacts table and past conversation participants) so you can type 'Sarah' instead of remembering the phone number - 'Connecting...' button now shows a spinner and times out after 15s with a clear error instead of hanging forever - Sign the .dmg container itself so Gatekeeper passes spctl --type install (notarizing only the inner app wasn't enough) Distribution: - Signed Developer ID Application: Max Ghenis (8VB5UKQZC6) - First properly notarized release \u2014 no Gatekeeper warning on first launch, no right-click \u2192 Open dance
OpenMessage v0.2.0 The big release: WhatsApp + Signal alongside Google Messages, full people-first sidebar, MCP-native, redesigned site. Highlights since v0.1.0: - WhatsApp live sync (whatsmeow linked-device) - Signal live sync via signal-cli bridge - People-first sidebar: one row per person, route tabs in the thread - Native macOS app refresh + .dmg packaging - Real-time SSE push updates in the web UI - Group SMS/MMS support via CLI and MCP - 30s incremental sync for WhatsApp/iMessage - 18+ MCP tools spanning search, drafts, sends, reactions, stats, and full narrative story generation - Encrypted waitlist storage on openmessage.ai - Comprehensive Playwright UI test suite (43 tests) - Site redesign with Next.js + Tailwind Distribution: - Cross-platform CLI binaries (darwin/linux × amd64/arm64) - macOS .app bundle + .dmg - Dockerfile + docker-compose for headless server use - Homebrew cask formula Privacy: - Opt-in anonymous heartbeat telemetry (off by default, OPENMESSAGE_TELEMETRY=1 to enable) - All message data stays local; no required cloud account
PreviousNext