Omi ships the same product on Flutter (iOS/Android), macOS, and Windows, backed by one Python backend. The recurring failure mode is a rule that gets fixed or changed on one platform and silently diverges on another: local-day grouping was fixed for the app in #10198 and again for macOS in #10980 and #10984, the Windows task bucketing comment claims it mirrors the Flutter app while the app actually uses a different model (see Divergence register below), and #11613 added a desktop route with no backend spec entry, breaking an inventory guard for every open PR.
This directory holds the shared, platform-neutral fixture set for those rules. Each platform runs the SAME vectors through its OWN production code in its own test suite. A behavior change now requires editing a fixture here, which shows up in review as a cross-platform decision instead of a single-platform drive-by.
| File | Rule under contract |
|---|---|
task_due_buckets.json |
Task due-date bucketing (Today / Tomorrow / Later / No deadline, and the overdue handling models) |
day_keys.json |
Local-calendar-day identity of a UTC instant (conversation day grouping) |
wire_action_item.json |
Action item wire decode: due_at instant equality across ISO offset forms, and the null / missing / unparseable agreement set |
section_labels.json |
Relative day labels (Today / Yesterday / Tomorrow) as calendar-day relationships, including DST transition days |
jit_runtime_contract_matrix.json |
Additive JIT ledger/evidence compatibility across legacy, v1, and future-version payloads |
conversation_duration.json |
The one duration a conversation reports: transcript span when segments exist, wall window only for transcript-free records |
| Platform | Suite | Runs |
|---|---|---|
| Backend/API and standalone MCP | backend/tests/unit/test_parity_contracts.py, backend/testing/contracts/test_jit_runtime_contract_matrix.py |
Backend unit suite and Desktop Backend Contracts CI |
| Flutter app | app/test/parity/parity_contracts_test.dart |
app/test.sh, CI Flutter tests |
| Windows desktop | desktop/windows/src/renderer/src/lib/parityContracts.test.ts, desktop/windows/src/shared/knowledgeLedger.test.ts |
npm test in desktop/windows, CI Desktop Windows tests |
| macOS desktop | JIT matrix: desktop/macos/Desktop/Tests/ServerMemoryV17DecodingTests.swift. Duration: desktop/macos/Desktop/Tests/ConversationDurationTests.swift. Task/day adapter remains pending. |
Desktop Swift CI |
| Web app | web/app/src/lib/__tests__/knowledgeLedger.test.ts |
web/app/test.sh, CI Web App checks |
The JIT runtime matrix is additionally consumed by the shipped mobile, macOS,
Windows, and web adapters plus the backend and standalone MCP suites. It proves
that a mixed response keeps all authoritative text readable, grants ledger
authority only to knowledge_ledger.v1, and makes future evidence inert by
mapping its semantics to unknown or omitting its references. It does not
activate JIT retrieval, trigger evaluation, frame requests, or any production
rollout gate.
The macOS adapter exercises both sides of that boundary: ServerMemory keeps
all text while recognizing only the v1 ledger authority, and ChatMessageDB
keeps chat text while ignoring the unrecognized evidence envelope rather than
projecting it into metadata or content blocks. This proves inert compatibility;
it does not claim that macOS renders structured chat evidence.
The backend suite validates every fixture file structurally (parseable, complete expectations, self-consistent day-key arithmetic) so a malformed fixture cannot pass vacuously on all clients at once, and pins the backend serialization side: due_at is always emitted as an ISO-8601 instant with an explicit offset. Naive datetimes are the one wire form the clients do NOT agree on (Dart and JS interpret them as local time, Swift ISO8601 decoding rejects them), so the backend emitting them is the bug the serialization contract exists to catch.
Divergences that exist in production today. Each is encoded in the fixtures rather than papered over; resolving one means changing the losing platform and updating the fixture in the same PR.
- Overdue model. The Flutter app (
app/lib/pages/action_items/task_categorization.dart) uses a separate Overdue bucket: past-due tasks go to Overdue, and tasks with no due date created more than 7 days ago age into Overdue. macOS (categoryFor,desktop/macos/Desktop/Sources/MainWindow/Pages/TasksPage.swift) and Windows (lib/taskBuckets.ts) fold past-due into Today and have no aging rule. The Windows comment previously claimed the fold model matched the Flutter app; it does not, andtask_due_buckets.jsonpins BOTH models per case so the difference is explicit until product picks one. - Missing created_at. The Windows sync mapper (
taskSyncEngine.tsmapBackendItem) fills a missing created_at with the sync timestamp because its local store requires one; the Dart wire model keeps it null. Consumers must not treat the Windows value as a backend fact. - Completed view overdue. The Flutter overdue branches are skipped when viewing completed tasks (a completed past-due task shows under Today, a completed stale dateless task under No deadline). Bucket fixtures therefore model the open-tasks view.
- Junk due_at strings (strict vs tolerant decode). A present-but-unparseable
due_at ("", "not-a-date") makes the Dart generated wire reject the WHOLE item
with a FormatException (its field reader treats a non-null field that reads to
null as invalid), while the Windows sync mapper maps it to no-due-date and keeps
the item. Found by this fixture set's first CI run: one corrupt timestamp in a
list response breaks the app's decode path but not Windows sync. The backend
sits on the strict side (it refuses to accept these forms, so it can never
re-emit them); the
expected_by_modelcases inwire_action_item.jsonpin both client behaviors until the platforms converge. - JIT empty watchlist — routing converged; ambient pacing missing on Windows.
macOS (
KnowledgeLedgerTriggerWatchlistRuntime,desktop/macos/Desktop/Sources/ProactiveAssistants/Core/KnowledgeLedgerTriggerRuntime.swift) routes a complete empty watchlist to the bounded ambient lane (owner decision 2026-09-01: an account with no standing trigger must not go silent). Windows (desktop/windows/src/shared/jitTriggerRuntime.tsevaluateJitWatchlist,desktop/windows/src/main/jit/jitRuntime.ts) now routes the same way: an empty complete watchlist evaluates toambient_fallbackand admits asno_eligible_planned_trigger, the one planned outcomeWindowsJitAssistanthands toadmitAmbient; theempty_watchlistsuppression reason is retired on both platforms. Routing is not the whole item: what the two platforms then spend in the ambient lane still differs, so item 5 is NOT converged. Open on Windows and tracked as the JIT client floor (decision 19):- No ambient pacing. macOS gates every ambient nano spend on
JITAmbientPacingPolicy(desktop/macos/.../JITAmbientPacingPolicy.swift: burst 2, then one peractiveDaySeconds / budget— two hours at the default eight — with 2 triages reserved for derived-intent matches), reached fromJITProactivityRuntime.swift:640-670. WindowsadmitAmbient(desktop/windows/src/main/jit/jitRuntime.ts) has no pacing at all: the local nano claim passesbudget: null, so the whole daily allowance can be consumed in the first minutes after local midnight — the exact failure measured on the owner account 2026-08-30/31 that produced the macOS policy. - No
ambient_server_deniedbackoff. macOS records a per-budget-day denial and suppresses forambientServerDenialBackoffafter the server refuses; Windows returnsreservation_already_consumedand retries on the next settled context. - No local per-day nano usage read before spend (macOS
readAmbientNanoUsage); Windows learns its position in the budget only from the server's answer. - Ambient context cooldown keys on an OCR-derived semantic fingerprint
(
WindowsJitAssistant.analyzehashes app + window title + OCR text), so a context whose text keeps changing produces a new fingerprint each frame and thereserveProactivityRPCs are unbounded per settled context — they continue after the server answersreserved: false. Pre-existing, not introduced by the routing change; it is what makes the missing pacing cost real money. - Budget day is
localBudgetDay(now)rather than the server'sbudget_timezone(macOS #12798).
- No ambient pacing. macOS gates every ambient nano spend on
- Malformed duration inputs.
conversation_duration.jsonpins only well-formed vectors. The backend helper (backend/utils/conversations/duration.py) and macOS (ServerConversation.durationInSeconds) validate each segment — empty text, non-finitestart/end, andend < startare ignored, and a wall window that ends before it starts clamps to 0 — while the FlutterServerConversation.getDurationInSecondstakes the raw maximumendover every segment and returns a negative wall duration unclamped. The losing platform is Flutter; converging it means changingapp/lib/backend/schema/conversation.dartand adding the malformed vectors to the fixture in the same PR.
- Edit the fixture file here. Keep times as local calendar components (or UTC instants
with per-offset expectations in
day_keys.json); never bake one zone's epoch values into a shared expectation. - Run the backend integrity test, then each platform suite. Every adapter loads the fixture by relative path from the repo root, so there is nothing to regenerate.
- If a platform legitimately disagrees, add a model column (as
task_due_buckets.jsondoes) and record it in the Divergence register with the owning files. Do not fork the fixture per platform.