Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

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:

  1. 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.
  2. Smart re-infer — the hasKeywordShift heuristic detects when recent messages introduce a new brand keyword that wasn't in the previous batch (e.g. 62438 started 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.
  3. App startup backfill — backfillAll() runs once at launch over every short-code conversation that has no inferred_contact_name yet. This is what populates the sidebar names on first run after the feature ships and after any future schema reset.
  4. 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).
  5. 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 / UNKNOWN result 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 is unref'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 existing inferred_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_name is 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 inferSmsContactName returns { 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 in displayContactName. 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. detectMultiBrand short-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, no contactName is 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 }; isTransientFailureReason classifies the callAi fallback reason
  • DB queries: /src/main/db/queries-sms/conversations.ts — setInferredContactName, listShortCodeConversationsNeedingInference, getConversationByPhone; insertMessage (messages.ts) triggers inferIfNeeded via dynamic import
  • IPC channel: SMS_UPDATE_CONTACT_NAME in /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-local isShortCode)
  • 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