Skip to content

Tags

Tags

v0.2.9

Toggle v0.2.9's commit message
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

Toggle v0.2.8's commit message
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

Toggle v0.2.7's commit message
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

Toggle v0.2.6's commit message
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

Toggle v0.2.5's commit message
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

Toggle v0.2.4's commit message
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

Toggle v0.2.3's commit message
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

Toggle v0.2.2's commit message
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>

v0.2.1

Toggle v0.2.1's commit message
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

v0.2.0

Toggle v0.2.0's commit message
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