Skip to content

feat: each message says whether its time has milliseconds, and a whole-second twin is a duplicate - #1968

Merged
mbeisser1 merged 53 commits into
mainfrom
feat/1923-time-precision
Oct 6, 2026
Merged

mbeisser1 merged 53 commits into
mainfrom
feat/1923-time-precision

Conversation

@mbeisser1

@mbeisser1 mbeisser1 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Feature Description

Each message in the conversation file now says whether its source recorded its time to the millisecond or in whole seconds (time_precision: seconds or milliseconds). The server stores the flag, answers it on the API, and an Export Run writes it back out. With the flag, the dedupe can hide a whole-second message as the duplicate of a message from the same source that matches it in everything else and has milliseconds in the same second, so the message is shown once, at its time to the millisecond. A millisecond time that ends in .000 is not taken for a whole second: the flag decides, never the value.

For people who import the same phone from backups that record time differently, for example SMS Backup+ mails timed by Date beside mails timed by X-smssync-date. Asked for by the decision of 2 October 2026 on #1096.

User Story

As a person who imports two backups of one phone, one that records whole seconds and one that records milliseconds, I want each message shown once, at its exact time, so that my conversation has no doubled messages and keeps the order the phone recorded.

Implementation Details

  • File format (schema version 11 → 12). IrMessage gains a required time_precision (message_ir::TimePrecision, now serde lowercase). A version-11 file is refused by its version (check_schema_version), never upgraded. one_copy_per_message now returns the precision it keeps with each time, so a whole-second copy that takes a millisecond copy's time (GO SMS Pro PDU beside the XML) takes the millisecond precision too.
  • Exporters. The projection already knew the precision (message_time); it is now written. iMazing, OpenExtract, GO SMS Pro PDU-only messages, and SMS Backup+ mails without X-smssync-date write seconds; Apple Messages, WhatsApp, SMS Backup & Restore, GO SMS Pro XML and SMS Backup+ with X-smssync-date write milliseconds. Each exporter's fixture test checks the field; SMS Backup+ gains a test for the Date fallback.
  • ir-format. CSV gains a time_precision column; EML and MBOX gain a required X-ME-Time-Precision header (the repo's X-ME- prefix; the issue text said X-MC-*). A blank, missing or unknown value is refused rather than guessed.
  • Server. messages.time_precision and staging_messages.time_precision (NOT NULL, CHECK in the two values). The import stages and promotes the flag; the message read path decodes it strictly.
  • Dedupe. flag_exact_content_key_dupes now reads time_precision. A new content_key_group_flags sets aside, within each content-key group, every whole-second message whose own source also holds a millisecond message in the group, runs the existing cross-source rule (exact_group_flags) over the rest, and hides each set-aside message under the rest's winner (always shown). The content key stays at whole seconds. A source that holds a message only in whole seconds, or only with milliseconds, keeps every copy as before.
  • Export. message-crate-export maps the API's time_precision back into the file.
  • Demo seed. Writes milliseconds (every source it imitates records milliseconds).

Tests, each verified to fail without the change it covers:

  • dedupe::tests::a_whole_second_message_is_the_duplicate_of_its_millisecond_twin_in_one_source and imports_api::tests::time_precision::a_whole_second_copy_and_a_millisecond_copy_are_shown_once_with_the_milliseconds (imported once from a whole-second file and once from a millisecond file, both orders, shown once with .250): fail with the twin rule disabled.
  • dedupe::tests::a_millisecond_time_ending_in_000_is_not_whole_seconds: fails with the twin rule disabled, and also fails under a rule that reads .000 from the time instead of the flag.
  • schema_version::tests::refuses_a_version_11_file_by_name: fails with SCHEMA_VERSION at 11.
  • imports_api::tests::time_precision::each_precision_is_kept_through_import_the_api_and_an_export_run (seconds and a .000 milliseconds message, through import, GET /v1/messages and GET /v1/exports/{id}/messages): fails when staging drops the flag.
  • export::project::tests::a_message_keeps_the_precision_the_server_stored: fails when the export ignores the stored flag.
  • ir-format every_format_keeps_whether_a_time_has_milliseconds (JSON, JSONL, CSV, EML, MBOX): fails when the CSV writer or the mail writer drops the flag.

Key Files Changed

  • crates/libs/ir/src/{lib.rs,identity.rs,projection.rs,schema_version.rs} - the field, version 12, precision through one_copy_per_message
  • crates/libs/ir-format/src/{write.rs,read_csv.rs}, crates/libs/mail/src/{headers.rs,lib.rs,parse.rs} - CSV column and X-ME-Time-Precision
  • crates/exporters/sms-backup-restore-exporter/src/read.rs, crates/exporters/imessage-ir-exporter/src/convert.rs - the two exporters that build messages outside the projection
  • schema/sql/messages.sql, schema/sql/staging.sql, crates/server/server/src/{models.rs,imports_api/staging.rs,db/staging.rs,db/conversation_messages.rs} - storage and the API
  • crates/server/server/src/dedupe.rs - the whole-second twin rule
  • crates/libs/api-types/src/lib.rs, crates/libs/export/src/project.rs - the API type and Export
  • docs/architecture/contacts-identities-and-messages.md - the dedupe rule and its reason

HTTP API Changes

  • Message.time_precision (new, required): seconds or milliseconds (new TimePrecision schema). Every route that answers a Message carries it: GET /v1/messages, GET /v1/conversations/{id}/messages, GET /v1/exports/{id}/messages, and the rest.
  • POST /v1/imports/{id}/batches: a message line without time_precision, or a header at schema version 11, is refused with 422 naming the line or the version.
  • OpenAPI (docs/src/assets/openapi.json) and web types (web/src/lib/serverApi.types.ts) regenerated.

Database Changes

  • No schema changes
  • schema/sql/*.sql changed (every database rebuilds empty and re-imports; the fingerprint is computed, nothing to bump)

messages.time_precision and staging_messages.time_precision, TEXT NOT NULL CHECK (time_precision IN ('seconds', 'milliseconds')).

How to Test

  1. cargo test -p message-crate-server --lib -- dedupe:: time_precision and cargo test -p message-ir-format -p message-ir -p message-crate-export.
  2. Export an SMS Backup+ directory holding one message twice, once with X-smssync-date and once with only Date, in two runs; import both with message-crate-server import (dedupe on) and check GET /v1/messages lists it once, with milliseconds and "time_precision": "milliseconds".
  3. Convert a conversation to CSV and to EML; check the time_precision column and the X-ME-Time-Precision header.

Checklist

  • Code follows the project's style guidelines
  • Self-reviewed the code
  • Added unit tests for new functionality
  • Added integration tests where applicable
  • Existing tests pass locally
  • Updated documentation
  • ./scripts/check-pr.sh passes and formatter rewrites are committed

Run locally on the head: ./scripts/check-pr.sh (pass), cargo test --workspace (2826 passed, 0 failed), cargo test --manifest-path src-tauri/Cargo.toml (pass), cd web && npm test (2062 passed) and npm run build (pass), ./scripts/check-generated-api-types.sh (match), cd docs && npm run check (0 errors) and npm run build (pass).

Related Issues

Closes #1923

Builds on #1096 (merged as #1963) and #1924 (merged as #1966). The branch was cut from #1966's branch and merged #1963's; both are now on main, and this pull request's diff against main carries none of their commits' changes.

Screenshots / Demo

No screen changes.

Deployment Notes

  • Schema version 12: conversation files at version 11 are refused when imported or converted; the backup must be exported again with this build.
  • The schema change rebuilds the database empty on first start; messages must be imported again.
  • The dedupe rule takes effect wherever the dedupe pass runs (an Import Run created with dedupe: true, message-crate-server import, dedupe-cross-source, reset-demo). The desktop app's push does not ask for dedupe today.
  • Known gaps, not handled here: an Apple Messages chat.db from before iOS 11 / macOS 10.13 records whole seconds but is written as milliseconds, because the Apple Messages Reader's protocol does not say which form it read (Apple Messages writes milliseconds for a chat.db that records whole seconds #1970). The desktop app's import never asks for dedupe, so the twin rule does not run through it (The desktop app's import never hides duplicates #1969). SMS Backup & Restore XML written back from the common message cannot carry the flag (documented in common-message.md).
  • A whole-second copy and a millisecond copy that both land on .000 share a guid; the stored message says milliseconds whichever came first (promote_time_precision, add_staged_copy_milliseconds).

🤖 Generated with Claude Code

mbeisser1 and others added 30 commits October 5, 2026 23:15
CONTEXT.md names the operation Export and lists Pull under its Avoid, but
the crate that runs an Export from a server, its types, the desktop
command, the state file and the recorded tool name all said Pull.

- The crate is message-crate-export at crates/libs/export/ (library
  message_crate_export), with ExportConfig, ExportReport, the private
  Export type, ExportJournalEvent, ExportJournalState and
  EXPORT_JOURNAL_NAME.
- The state file is .message-crate-export-state.jsonl. The old file is
  not read.
- The server records the tool as message-crate-export. The old name is
  not mapped.
- The desktop command is export (src-tauri/src/commands/export.rs,
  ExportArgs), and the web app calls it as invokeExport.
- CLAUDE.md, AGENTS.md, the developer pages, the Export guide, the
  OpenAPI document and the crate READMEs name the new crate. ADR 0001 and
  ADR 0012 keep their text and gain a dated note.

Closes #1906

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md keeps file paths, tool names and crate names out of CHANGELOG
entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Export's own working directory and the step that fills it said pull,
which CONTEXT.md avoids for Export. The directory is .exported (EXPORTED,
the exported field on ExportDir), and the step is the fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server stored a message's time as whole seconds, dropping the
milliseconds WhatsApp, Apple Messages and SMS Backup & Restore record, so
two messages in one second were ordered by sort_order alone.

messages.timestamp (and staging_messages.timestamp and
message_versions.edited_at) now hold RFC 3339 UTC with three fractional
digits, such as 2015-03-12T18:04:22.250Z, always in that one form so the
text still sorts in time order. Lists order by it as before, and the API
returns it in Message.timestamp and edited_at. The content key and the
near-time pass still match at whole seconds.

A search's day bounds are written in the same millisecond form, since a
bound without a fraction sorts after a stored time of the same second.
The search for the end of a time zone's gap now steps in whole seconds,
because a half second left in it would now show in the bound.

Tests: a fixture with sub-second times keeps them through import and the
API, two messages 300 ms apart list in time order on both message routes,
and a day begins at its first millisecond in search. Each fails without
the change. The schema change rebuilds an existing database.

Closes #1096

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch already names fetching an Asset, so the step before the format step
is the export step, after the invokeExport call it makes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search day bound, format_utc_timestamp and the tests that build
stored times now all call models::utc_timestamp_text, so the form that
text comparison relies on is defined once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExportMeta gains backup_taken_at_unix_ms, and schema_version goes from 10
to 11; a version-10 file is refused by its version. Each exporter fills
the date from its source: an iPhone backup's Manifest.plist date for
Apple Messages and WhatsApp, a Mac chat.db's modification time, the
backup_date attribute of an SMS Backup & Restore file, the newest
modification time of the files read for iMazing, OpenExtract, GO SMS
Pro, SMS Backup+ and an Android WhatsApp database. The demo seed dates
its backups at its reference time.

ir-format carries the date in every format it writes and reads: JSON
and JSON Lines through serde, CSV in a backup_taken_at_unix_ms column,
EML and MBOX in an X-ME-Backup-Taken-At-Unix-Ms header. A value that is
not a whole number is refused rather than read as no date.

Part of #1924.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staging keeps each staged row's backup date (staging_messages
.backup_taken_at), and messages keeps the date of the backup that
decided the stored copy. Between two copies of one message from one
source, when both backups have a date, the copy from the later backup
gives the message its deletion mark, mark or no mark, and its text and
earlier versions, whatever the versions' times say; a copy from an
earlier or the same backup changes neither. This holds in one import in
either file order and across imports in either order, so the duplicate
flag, which follows the text, does too. Where either backup has no date,
the rules for files without one hold: a mark adds, now also from the
second file of one import, and an edit is taken when its newest earlier
version is newer.

The Import Run records the newest backup date its files name, and the
HTTP API answers it as backup_taken_at on ImportRun and
ImportRunSummary. A message answers its own backup_taken_at, which an
Export Run writes back into the conversation file as the newest date of
the conversation's messages.

The schema change rebuilds the database; nothing migrates.

Part of #1924, #1741, #1804.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…made

Settings → Storage → Import history opens a run's details with a Backup
line: the backup the desktop app recorded for the run, and when that
backup was made, from the run's new backup_taken_at. A backup that
records no date says so. The owner's view of another account's run
carries neither and shows nothing.

Part of #1924.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The common message page documents export.backup_taken_at_unix_ms,
where each exporter reads it from, and the rule the import applies; the
CSV and mail references list the new column and header; the import and
storage pages say which backup wins and where the date shows. The
CHANGELOG entry for 0.11.0 says files at schema version 10 are refused.

Closes #1924.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es' into feat/1923-time-precision

# Conflicts:
#	CHANGELOG.md
#	crates/server/server/src/db/staging.rs
…x row back

#1957 reindexes a stored message only when a staged row carries a mark,
because a promotion never cleared one. With #1924 a later backup can clear
an Unsent mark, and that message stayed out of the search index. The
promotion now names every message whose mark it changes, set or cleared,
in _promote_mark_map, and the reindex set reads that table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge of #1924 with #1096 left the backup date going through the
millisecond time writer as seconds, so every backup read as 1970. It now
passes milliseconds, and the stored date takes the three-digit form every
stored time takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out one

Two reads of one Mac's chat.db within one second carry the same date,
and the later-backup rule read them as the same backup, so an unsend or
an edit between them was lost. Equal dates now cannot decide, in SQL
(later_backup_sql) and in Rust (the new later_backup helper, which the
in-import copy rule uses instead of restating the comparison), and the
undated rules hold: they change nothing for the same backup read again.
add_staged_copy_mark no longer returns a flag its caller drops. The
architecture doc says the date is kept to the second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Messages keeps chat.db in write-ahead mode, so a row written since the
last checkpoint moves only chat.db-wal's time, and two reads of one Mac
could carry the same date or an iPhone backup could read as later than
a deletion the Mac already held. The date is now the newest
modification time of chat.db, chat.db-wal and chat.db-shm. The format
doc also says equal dates fall back to the rules for files without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sages share it

An Export Run stamped every message of a conversation file with the
newest backup date of any of them. Imported elsewhere, a message decided
by an older backup then claimed the newest date and ignored a backup made
between the two, and a message with no date gained one. The file carries
one date for all its messages, so it now names one only when every
message has the same, and none otherwise, which keeps the rules for
files without a date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SMS Backup & Restore writer left backup_date off the root element,
so an Export or conversion to XML read back in was dated by when Message
Crate wrote the file and read as the newest backup. The writer now
writes the newest backup date of the conversations it holds, the rule
the reader follows for a conversation read from two files, and leaves
the attribute out when none has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbeisser1 and others added 10 commits October 6, 2026 00:33
…n-at

#1963 made every stored time three-digit milliseconds and changed
format_utc_timestamp to take milliseconds. The backup date now goes to
it whole, so it is stored to the millisecond like every other time, and
the docs that said "to the second" say so. CHANGELOG keeps both sides'
Upgrading entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A message in the conversation file gains a required time_precision,
seconds or milliseconds, and schema_version goes from 11 to 12; a
version-11 file is refused by its version. The flag, never the time,
says whether a time has milliseconds, since a millisecond time can end
in .000.

Every exporter writes the precision it already knew: iMazing,
OpenExtract, GO SMS Pro's PDU files and SMS Backup+ without
X-smssync-date write seconds; Apple Messages, WhatsApp, SMS Backup &
Restore, GO SMS Pro's XML and SMS Backup+ with X-smssync-date write
milliseconds. When an exporter keeps one copy of a message, a
whole-second copy that takes a millisecond copy's time takes its
precision too. The demo seed writes milliseconds.

ir-format carries the field in every format: JSON and JSON Lines
through serde, CSV in a time_precision column, EML and MBOX in an
X-ME-Time-Precision header. A blank or unknown value is refused rather
than guessed.

Part of #1923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two paragraphs the merge of #1963 edited ran past the wrap width. Test
fixtures that stand for a stored backup date now use the three-digit
millisecond form the server writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
staging_messages and messages gain time_precision, seconds or
milliseconds, from the conversation file, and the HTTP API answers it
as time_precision on a Message. An Export Run writes the stored value
back into the conversation file. The schema change rebuilds the
database; nothing migrates.

Part of #1923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s millisecond twin

Within one source, a message whose time is in whole seconds is hidden as
the duplicate of a message that matches it in everything else and has
milliseconds in the same second, so an SMS Backup+ message imported once
from a mail timed by its Date header and once from one timed by
X-smssync-date is shown once, with its milliseconds. Before, the exact
pass hid a duplicate only when another source held it. The content key
stays at whole seconds, and the stored flag decides, never the time: a
millisecond time that ends in .000 is not taken for a whole second.

Part of #1923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The common message page, the export structure, the CSV columns, the mail
archive headers and the message transfer page describe time_precision at
schema version 12. The architecture note gains the rule that, within one
source, a whole-second message is the duplicate of its millisecond twin,
with its reason. The changelog says version-11 files are refused.

Part of #1923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…feat/1923-time-precision

# Conflicts:
#	CHANGELOG.md
#	crates/libs/api-types/src/lib.rs
#	crates/server/server/src/db/staging.rs
#	crates/server/server/src/db/staging/tests.rs
#	crates/server/server/src/test_support.rs
#	crates/server/server/tests/fixtures/apple-messages-sub-second-times.jsonl
#	web/src/lib/serverApi.types.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
origin/main gained #1966 as one squashed commit whose tree is the
feat/1924-backup-taken-at head this branch already merged, so this branch's
tree stands as it is.

@mbeisser1 mbeisser1 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of 19ff774: Standards, Spec and Correctness findings on their lines below. Findings with no line in the diff follow in a top-level comment.

Comment thread crates/libs/export/src/project.rs Outdated
Comment thread CHANGELOG.md Outdated
Comment thread crates/libs/ir/src/identity.rs
Comment thread crates/server/server/src/dedupe.rs Outdated
Comment thread crates/server/server/src/dedupe.rs
Comment thread crates/exporters/imessage-ir-exporter/src/convert.rs
Comment thread crates/exporters/imessage-ir-exporter/src/convert.rs
Comment thread crates/server/server/src/dedupe.rs
Comment thread docs/src/content/docs/docs/developer/architecture/common-message.md
@mbeisser1

Copy link
Copy Markdown
Member Author

Findings with no line in the diff at 19ff774.

F1 · Spec · spec (crates/libs/push/src/http.rs line 301). Spec: "An SMS Backup+ message imported once from a whole-second file and again from a millisecond file is shown once, with the millisecond time." create_import sends only source and mode; CreateImportRequest.dedupe defaults to false, and nothing in src-tauri or push sets it. Through the desktop app, the product's import path, both copies stay shown, and not only on .000: the whole-second copy's guid is keyed at .000, so it differs from the .250 copy's guid and an append stores both. a_whole_second_copy_and_a_millisecond_copy_are_shown_once_with_the_milliseconds passes only because it creates the run with "dedupe": true. Fix: run the within-one-source twin rule on every promote whether or not dedupe is on (it only flags rows inside one source), or have the push client send dedupe: true; add a test that imports with the push library's defaults.

F2 · Spec · judgement (crates/server/server/src/imports_api/staging.rs). A whole-second copy and a millisecond copy that ends in exactly .000 share a guid, so an append keeps whichever came first. If the whole-second copy came first, the message stays seconds though a millisecond copy was imported. Fix: on a guid match where the stored row is seconds and the incoming one is milliseconds, set the stored row to milliseconds; or file an issue.

F3 · Correctness · bug (crates/server/server/src/db/staging.rs line 1338, the promote's guid match). Source A imports a whole-second copy W at X.000 (an SMS Backup+ mail with only Date). A later append of the same source brings the millisecond copy at X.000 (with X-smssync-date). Same guid, so promote keeps the stored row, and time_precision stays seconds for good; the API and every export say seconds for a time the source recorded to the millisecond. Fix: on a guid match, raise the stored row to milliseconds when the staged copy says milliseconds: one UPDATE over _promote_msg_map, like the backup_taken_at update.

mbeisser1 and others added 8 commits October 6, 2026 01:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each gains ALL and parses by finding the variant whose name matches,
as Deletion does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pass

It was compared as text, so an unknown value counted as milliseconds,
where the message read path refuses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s copy wins

The twin is hidden under the cross-source winner, which pick_winner
ranks by attachments and import order, not precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…precision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds says milliseconds

The two copies share a guid, so the copy stored first kept its flag and
a whole-second copy stored first stayed seconds for good. A staged or
promoted copy that says milliseconds now raises the stored flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbeisser1 and others added 2 commits October 6, 2026 01:12
…ssage's time

A copy cut to the second elsewhere that kept the guid of a message with
other milliseconds was marked milliseconds at a .000 time it never had.
promote_time_precision returns nothing, and its phase names what it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are rewrapped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mbeisser1
mbeisser1 marked this pull request as ready for review October 6, 2026 05:21

@mbeisser1 mbeisser1 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the fix commits (Standards and Correctness), pinned to f833bc8. Each is fixed in this push; replies follow in each thread.

Comment thread CHANGELOG.md
Comment thread CHANGELOG.md
Comment thread crates/server/server/src/imports_api/promote.rs
Comment thread docs/architecture/contacts-identities-and-messages.md
Comment thread crates/server/server/src/imports_api/staging.rs
Comment thread crates/server/server/src/dedupe.rs
Comment thread crates/server/server/src/db/staging.rs
@mbeisser1

Copy link
Copy Markdown
Member Author

Answers to the findings in #1968 (comment):

F1 · Spec · spec (crates/libs/push/src/http.rs line 301). … create_import sends only source and mode; CreateImportRequest.dedupe defaults to false …

Deferred to #1969. The twin rule runs wherever the dedupe runs (the server's import, dedupe-cross-source and reset-demo commands, and an Import Run created with "dedupe": true), and #1923 places it in the dedupe ("Today dedupe_cross_source … flags a duplicate only when another source holds it"). That the desktop app never asks for dedupe predates #1923 and holds for the cross-source rule too; choosing between the app asking for it and the server running one-source rules on every promote changes every import, so it is filed rather than decided here.

F2 · Spec · judgement (crates/server/server/src/imports_api/staging.rs). A whole-second copy and a millisecond copy that ends in exactly .000 share a guid, so an append keeps whichever came first …

F3 · Correctness · bug (crates/server/server/src/db/staging.rs line 1338, the promote's guid match). … time_precision stays seconds for good …

Fixed in 6bfdb84, with the time guard in d6f19fb: promote_time_precision in db/staging.rs raises a stored message to milliseconds when its staged copy is milliseconds at the same time, and add_staged_copy_milliseconds does the same for two copies in one import. one_message_held_at_whole_seconds_and_at_000_milliseconds_says_milliseconds (both orders, one import and two) fails without either half; a_whole_second_copy_at_another_time_keeps_seconds covers the guard. The architecture doc states the rule.

@mbeisser1

Copy link
Copy Markdown
Member Author

Review summary for f833bc8

Merge check before the review: the branch recorded #1966's squash (7a8435e) with git merge -s ours. The squash's tree equals fea14d2, the #1966 branch head this branch had already merged in de31ed4, and origin/main is an ancestor of the head. Every file in git diff origin/main HEAD is #1923 work, and no hunk reverts anything main has. The PR was up to date with main, so no merge of the base was made, and none was needed before merging.

Findings, by axis:

Totals: 16 Fixed, 0 Declined, 3 Deferred (#1969, #1970, with two findings sharing #1970).

No commits were needed for CI failures: the ready run on f833bc8 passed. No merge of the base was made. There was a Spec review. No user threads are open.

@mbeisser1
mbeisser1 merged commit a32281f into main Oct 6, 2026
28 checks passed
@mbeisser1
mbeisser1 deleted the feat/1923-time-precision branch October 6, 2026 05:35
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.

The conversation file says whether a message's time has milliseconds, and a whole-second twin is a duplicate

1 participant