SMS short-code name inference (auto-fill descriptive contact names)
When an SMS conversation comes from a short code (a 3–6 digit sender like 62438 or 22395, no + prefix), Omniscio auto-fills a descriptive contact name for it — 62438 becomes "eBay security code", 22395 (which mixes Amazon, eBay, and Walmart codes) becomes "Verification codes (multi-brand)".
What it is
What it does
When an SMS conversation comes from a short code (a 3–6 digit sender like 62438 or 22395, no + prefix), Omniscio auto-fills a descriptive contact name for it — 62438 becomes "eBay security code", 22395 (which mixes Amazon, eBay, and Walmart codes) becomes "Verification codes (multi-brand)". The inference runs against the conversation's recent message bodies via Qwen3-32B on OpenRouter (~$0.0001/call, same path as title generation), and the result is stored alongside any manual contact name you set. Manual edits always win — the AI's guess is shown only when no human-set name exists, and the moment you save a name it sticks forever.
What you see
In the SMS sidebar, short-code conversations now show a friendly label instead of a raw number. New users get the inferred names backfilled the first time Omniscio starts up after the feature ships — within ~30 seconds of launch you'll see the names appear in place. Numbers with a manual contactName already set are untouched. Numbers that aren't short codes (regular phone numbers like +1 555-…) keep their existing display behavior — inference is short-code-only.
When you open a conversation whose displayed name is the AI's guess, a small pencil icon appears in the viewer header next to the name. Click it to swap the title for an inline input pre-filled with the inferred name. Type your correction, press Enter to save, or Escape to cancel. After save, the pencil disappears — your manual name has taken over and the AI guess is no longer used. The pencil never appears for conversations that already have a manual name (nothing to fix) or for non-short-code numbers (no inferred name in the first place).
If a short code carries verification codes from multiple brands (heuristic: 2+ recognizable brand keywords across the recent messages — Amazon + eBay + Walmart, etc.), the model is skipped entirely and the conversation is labeled "Verification codes (multi-brand)" literally. This is both a cost saver and a more honest label — there's no single brand to name.
Where to find it
In the SMS sidebar: short-code conversations show a friendly label instead of a raw number, and the pencil icon beside the name lets you correct it.
How it behaves
Triggers
Inference runs in five situations, all non-blocking:
- Per-message — every inbound SMS calls
inferIfNeeded(phoneNumber)from the message-insert path in /src/main/db/queries-sms/messages.ts via dynamic import (avoids a circular dep). The service then walks five skip rules: not a short code → skip; manual contact_name set → skip; no messages yet → skip; already inferred AND no keyword shift in the last 3 messages → skip; daily cost cap hit → skip. Only conversations that pass every rule reach the model. - Smart re-infer — the
hasKeywordShiftheuristic detects when recent messages introduce a new brand keyword that wasn't in the previous batch (e.g.62438started as eBay-only and now carries USPS codes too). When that happens, even an already-named conversation gets re-inferred so the label keeps up. - App startup backfill —
backfillAll()runs once at launch over every short-code conversation that has noinferred_contact_nameyet. This is what populates the sidebar names on first run after the feature ships and after any future schema reset. - Manual force re-infer —
forceReinfer(phoneNumber)bypasses every skip rule. Wired through but not exposed in the UI yet (a future "Re-guess name" button could call it). - Transient-failure auto-retry — when an attempt can't reach the model (request timeout, open circuit breaker, or offline — distinct from the model responding "I can't name it"), the service schedules a bounded, backed-off retry via
scheduleTransientRetry: up to 3 attempts at ~1 → 2 → 4 min, then it gives up. A definitive empty /UNKNOWNresult is not retried (re-billing a genuinely-unnameable code every cycle would burn the cost cap). This closes a gap where a one-message short code — e.g. a single verification code that never texts again — whose first call timed out stayed a raw number forever, re-attempted only on a new inbound message or the next startup backfill. The retry timer isunref'd so it never blocks app shutdown; a retry lost to a restart is recovered by trigger 3. Locked by the retry contract.
Storage
Migration v157 adds an inferred_contact_name TEXT NULL column to the sms_conversations table. The display fallback chain is centralized in /src/renderer/src/lib/phone-format.ts:
displayContactName(conv) =
conv.contactName // manual override — wins always
?? conv.inferredContactName // AI's guess — shown when no manual name
?? formatPhoneNumber(conv.phoneNumber) // raw fallback (see short-code note)
Short-code raw display. When the fallback runs for an as-yet-unnamed short code, formatPhoneNumber short-circuits to the raw digits (24273) and skips libphonenumber entirely. Without that, a short code whose leading digits match a NANP area code (e.g. 242 = Bahamas) was parsed as a real number and shown as a misleading +1 24273. The check is a renderer-local isShortCode mirroring main's phone-utils.isShortCode (main's file imports Electron and can't cross into the renderer — the same reason getDefaultCountryRegion is duplicated there); keep the two in sync.
Manual edits go through the SMS_UPDATE_CONTACT_NAME IPC channel (/src/shared/ipc-channels/index.ts), which writes contactName and leaves inferredContactName intact. NULL-safe re-infer means a model returning empty/garbage never overwrites a known-good name — the existing inferred name is preserved.
Cost
Each inference call hits Qwen3-32B on OpenRouter (the same cheap model Omniscio's title generation uses). Per-call cost is roughly $0.0001. Spend is logged to api_cost_log under the source label 'sms-name-infer' via the existing trackApiCost infrastructure, so it shows up in the Cost dashboard alongside every other Anthropic/OpenRouter call.
A daily spend cap lives at Settings → SMS → "AI name inference daily cost cap (USD)" — default $0.10, range $0.05 – $5. The pre-flight check in inferIfNeeded blocks the model call once cumulative 'sms-name-infer' spend for the calendar day hits the cap; inference resumes the next day. A first-launch backfill across, say, 50 short-code conversations costs roughly $0.005 — well under the default cap.
Failure modes
- Cost cap hit. The service logs
[sms-name-infer] daily cost cap hit ($X.XX) — skipping inference for <phoneNumber>and returns early. The conversation keeps its existinginferred_contact_name(or stays without one). Inference resumes automatically on the next calendar day. - Model returns empty / too short (DEFINITIVE). NULL-safe re-infer: the existing
inferred_contact_nameis left alone (don't overwrite a known-good name with garbage). This is a definitive outcome — the model responded but couldn't name it — so it is not auto-retried (a timer retry would just re-ask about a genuinely-unnameable code and burn the cost cap). With no prior name, the conversation stays unnamed and falls back to the raw phone display; a later inbound message or a keyword shift can still re-trigger it. - Network down / OpenRouter unreachable / timeout (TRANSIENT). The model couldn't be reached, so
inferSmsContactNamereturns{ transient: true }and the service schedules a bounded backed-off retry (trigger 5 — up to 3 attempts, ~1 → 2 → 4 min) instead of stranding the conversation. Until a retry lands it falls through to the raw phone display indisplayContactName. An open circuit breaker counts as transient too. (Before this, a timed-out call was treated like "no name" and only re-tried on the next inbound message — a one-shot short code stayed a number.) - Multi-brand detected.
detectMultiBrandshort-circuits before the model call — no API spend, label written as"Verification codes (multi-brand)". This is the expected outcome for shared short codes (some carriers route multiple brands through the same number). - Pencil-edit save fails. The viewer toasts
"Failed to save name: <error>"via the toast store and stays in edit mode so you can retry. The IPC call is the single source of truth — until it succeeds, nocontactNameis written, so the inferred-name display is preserved.
For agents
Code map
- Service: /src/main/services/sms/sms-name-inference-service.ts —
inferIfNeeded,forceReinfer,backfillAll;scheduleTransientRetry+transientRetryAttempts(the bounded retry,MAX_TRANSIENT_RETRIES/RETRY_BASE_DELAY_MS) - Heuristics: /src/main/services/sms-name-inference/heuristics.ts —
detectMultiBrand,hasKeywordShift - Short-code check: /src/main/services/phone-utils.ts —
isShortCode(main); mirrored renderer-local copy in the display helper below - Model call: /src/main/services/ai/ai-suggestion-service.ts —
inferSmsContactName(phoneNumber, recentMessages)returns the name (string),null(definitive), or{ transient: true };isTransientFailureReasonclassifies thecallAifallback reason - DB queries: /src/main/db/queries-sms/conversations.ts —
setInferredContactName,listShortCodeConversationsNeedingInference,getConversationByPhone;insertMessage(messages.ts) triggersinferIfNeededvia dynamic import - IPC channel:
SMS_UPDATE_CONTACT_NAMEin /src/shared/ipc-channels/index.ts - Display helper: /src/renderer/src/lib/phone-format.ts —
displayContactName;formatPhoneNumber(short-circuits a bare short code to raw digits via the renderer-localisShortCode) - Pencil edit UI: /src/renderer/src/features/sms/SmsViewer.tsx —
showPencil,startEditName,saveName,cancelEditName - Settings: declarative entry in /src/renderer/src/features/settings/sections/sms/SmsSettings-definitions.ts (
settingsKey: 'smsNameInferCostCapUsdPerDay') - Migration: v157 in /src/main/db/incremental-migrations.ts
Related
- sms-name-inference-retry-contract.md — the test-locked invariants for the transient-retry distinction and the short-code raw display.
- set-up-sms-integration.md — the underlying SMS integration (Pushbullet or native) this feature decorates.
- workflow-coach.md — same daily-spend-cap pattern (per-day Haiku budget with a pre-flight check); the Workflow Coach was the canonical reference implementation copied here.
Last verified 2026-09-23