Skip to content

Add openmessage v2 reconcile-signal to top up v2 from legacy Signal history (draft) - #189

Draft
MaxGhenis wants to merge 1 commit into
mainfrom
feat/signal-reconcile
Draft

MaxGhenis wants to merge 1 commit into
mainfrom
feat/signal-reconcile

Conversation

@MaxGhenis

Copy link
Copy Markdown
Owner

Draft. This adds openmessage v2 reconcile-signal, a local-only maintenance command that copies Signal messages from the legacy store into v2 when v2 is missing them. It targets the gap left by the 7/23–7/25 Signal v2 projection stall: the legacy path kept ingesting while the v2 read projection stood still. It has not been run against a real install. It needs a review, and then a deliberate run by Max after the Signal relink.

What it does

openmessage v2 reconcile-signal [--from <dir>] [--since YYYY-MM-DD] [--dry-run] [--json]

  • Reads Signal messages from the legacy messages.db and imports any that v2 lacks through the v2 historical-import seam. It uses the same natural keys as live ingest, so a repeat run or an interrupted run converges instead of duplicating. That includes the byte-identical legacy source IDs and outgoing local: aliases (internal/reconcile/signal.go).
  • Requires the existing signal-primary account with the signal_cli bridge key. It never creates or enables transport state, and it touches no credentials or network.
  • Corrects conversation kind from the authoritative Signal remote prefix (signal: vs signal-group:). It never links a group or the self account as a direct peer. It keeps messages with missing or malformed sender metadata, using a null sender identity.
  • Media is not copied. Each deferred attachment is counted in the report (media_deferred).
  • --since limits the scan to legacy messages at or after local midnight on that date.
  • stdout is always exactly one JSON value, the report: conversations_scanned, conversations_created, messages_scanned, messages_imported, messages_already_present, media_deferred, skipped, and skip_reasons. Human guidance goes to stderr. Exit codes: 3 = source/usage problem, 4 = lock or running backend, 5 = run failure.

Safety guards (in cmd/reconcile.go)

  • It refuses to run next to a live daemon. It takes the data directory's instance lock and exits 4 if the lock is held. It then probes the backend URL (OPENMESSAGES_HOST/OPENMESSAGES_PORT, default 127.0.0.1:7007) and exits 4 if a backend answers, or if it cannot rule one out.
  • Read-only where possible. The legacy store is always opened through a new repair-free read-only path (db.OpenReadOnly). v2 is opened read-only under --dry-run (sqlite.OpenReadOnly). --dry-run performs every read and derivation without mutating either store, and a test pins that immutability.

How Max would run it (after the Signal relink, d328)

  1. Stop the watchdog from relaunching the app while it is quit. If you are on the hardened watchdog: printf 'max-age=2h\nsignal reconcile\n' > ~/Library/Application\ Support/OpenMessage/watchdog-disabled
  2. Quit OpenMessage.app. The command refuses to run while the daemon is up.
  3. Dry run first: openmessage v2 reconcile-signal --dry-run > /tmp/reconcile-dry.json, then read messages_imported (what a real run would add) and skip_reasons.
  4. If the numbers look right, run it for real without --dry-run. A second real run should report messages_imported: 0.
  5. Relaunch the app and remove the flag.

Provenance

The work was done on 2026-07-26 on wip/signal-reconcile (7eac32f), on top of the #155 branch. #155 later landed as the squash commit b02aabb (#156), so that branch now carries pre-squash duplicates of #155 and conflicts with main. This PR carries only the reconcile work: the net diff b95775c..7eac32f, where b95775c is the #155 tip, minus the agent's PROGRESS.md ledger. Applied to current main (2b09b61), only main.go conflicted: main had added the repair command in the same two spots, and both are kept. wip/signal-reconcile is left untouched.

Tests

The new tests cover:

  • key parity with live ingest, import and second-run idempotence, twins that already exist, and fallback keys
  • skip reasons, deferred media, --since, and dry-run immutability
  • account and participant safety, backend refusal, read-only routing, and partial reports
  • a real sink/decoder/worker outgoing replay (internal/ingest/reconcile_signal_test.go).

Local results at af46a22 on 2026-09-25 (macOS, loopback listeners allowed, unlike the July sandbox):

  • GOWORK=off go build ./... and go vet on the touched packages: clean. gofmt -l: clean.
  • GOWORK=off go test ./internal/reconcile/ ./internal/db/ ./internal/storage/sqlite/ ./internal/ingest/ ./internal/v2read/ -count=1: all ok.
  • GOWORK=off go test ./cmd/ -run '(?i)signal|reconcile' -count=1: ok.
  • Full GOWORK=off go test ./... -count=1: all 34 packages with tests pass. The July run could not do this because its sandbox blocked loopback listeners.

Open questions for review

  • Should this live under openmessage v2 … (it is the first v2 subcommand) or next to repair google-idspace as repair signal-history?
  • Has anything since July already backfilled the stall window? If so, the dry run will show messages_already_present and nothing to import, and this can be closed.
  • The runbook does not document the command yet. Add a section once the name is settled.

🤖 Generated with Claude Code

… history

A local-only maintenance command for the gap the 7/23-7/25 Signal v2
projection stall left: the legacy store kept ingesting while v2 did not.
It imports legacy Signal messages that v2 lacks through the historical-import
seam with live ingest's natural keys (idempotent; repeat and interrupted runs
converge), requires the existing signal-primary/signal_cli account, corrects
conversation kind from the Signal remote prefix, keeps malformed-sender
messages under a null sender identity, and defers media (counted in the
report).

Safety: takes the data directory's instance lock and refuses (exit 4) if it
is held or if a backend answers on OPENMESSAGES_HOST:OPENMESSAGES_PORT; opens
legacy through a new repair-free read-only path (db.OpenReadOnly) and v2
read-only under --dry-run (sqlite.OpenReadOnly). stdout is exactly one JSON
report; guidance goes to stderr.

Ported from wip/signal-reconcile (7eac32f, 2026-07-26), which was built on
the #155 branch before #155 landed as the squash b02aabb (#156). This commit
is the net diff b95775c..7eac32f (b95775c = the #155 tip) without the agent's
PROGRESS.md ledger, applied to 2b09b61. main.go conflicted only because main
added `repair` in the same spots; both commands are kept.

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

This branch has not been deployed

No deployments
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