Changelog
What's new.
Every improvement to the platform, as it ships.
· kamo-internal
SI routing shows what SIs use now — the effective model per kind of work and whether the org's order or the automatic default chose it; for speech-to-text, the model on phone calls and in meetings, streamed or per sentence, with the automatic accuracy-ranking hint
Owner 2026-10-15: "If a model is added and enabled to the SI Providers that supports speech to text, it should use the best one." Reads AIService's effective / effectiveCalls / effectiveMeetings on each routing entry; an AIService without them shows nothing new.
· EmailService
Email unsubscribes reach the do-not-contact list, and campaigns, shares and an SI's mail check it
Owner 2026-10-15: one company-wide do-not-contact list per organization, scoped per channel; an email unsubscribe blocks email only. - Writer: MirroringEmailOptOutLedger (the EmailOptOutLedger bean) hands every applied change to EmailDoNotContactMirror. A whole unsubscribe (preference page, one-click, List-Unsubscribe mailto, relay) lists the address for EMAIL (source EMAIL_UNSUBSCRIBE, recorded by "Recipient (<source>)" or "Mail system (<source>)"); the person allowing mail again takes it off (EMAIL_RESUBSCRIBE), a member lifting it with the person's consent takes it off with their note (MANUAL). Bounces, complaints, topics and pauses stay the email list's own. Written after the opt-out commits, REQUIRES_NEW, never thrown: an unsubscribe never fails because of the mirror. - Readers: SuppressionService (campaigns, article shares) and SuppressionLookup (eligibility) also count the EMAIL entries, so an address added or imported in Settings -> Phone -> Do not contact is withheld from marketing mail. POST /api/email/send refuses an SI (mail_send, mail_reply, mail_forward) 409 {reason: DO_NOT_CONTACT, error, addresses} on the final recipients, 503 when the list cannot be read; a person's own correspondence keeps the existing rules. Tests: EmailDoNotContactMirrorTest, DoNotContactSuppressionTest, SiMailDoNotContactGuardTest, EmailControllerSendContractTest (RED on origin/main, GREEN here), the other EmailController tests, DeliverabilityWiringTest, SuppressionLookupTest, BulkEmailWiringTest, the arch ratchets.
· VOIPService
The organization's do-not-contact list — VOIPService owns it, texts check it, STOP and START write it
Owner 2026-10-15: one company-wide do-not-contact list per organization, scoped per channel ("Only that channel"). - DDL **************** (DO_NOT_CONTACT, DO_NOT_CONTACT_EVENT append-only for kamo_app, DO_NOT_CONTACT_SETTINGS), applied 2026-10-15 04:35Z. - DoNotContactBeans declares shared-lib's DoNotContactList (repositories already inside the com.kamo.z.shared.comms scan; VoipRepositoryScanTest pins them). - /api/voip/do-not-contact (MANAGE_VOIP_SETTINGS): list/search, add, import (up to 1000 lines), POST /{id}/remove with a required reason, history, activity, settings (call mode WARN | BLOCK, default WARN). GET /check needs only the session: what the softphone and the mobile app ask before dialling ({listed, recordedAt, source, mode, message: "{number} asked not to be called on {date}."}). - Lookup for other services: GET **************** ?orgId=&channel=&address= (KAMOAI_INTERNAL_SECRET); a service with the shared-lib scan calls **************** channel, address). - Texts: SmsGateway refuses a number listed for SMS with OPTED_OUT (never retried) for every sender, a person's or an SI's sms_send; the HELP reply is exempt (Attempt.keywordReply). Bulk /send and /test-send refuse it with 409. Transactional template and internal sends keep their existing rules. - STOP lists the number for texts (source SMS_STOP); START/UNSTOP takes it off for texts only (SMS_START, reason "Texted START"). The STOP projection on AGENT_CONTACT_STATE now sets texts only: email, chat and social keep their own opt-outs (owner: a STOP text blocks texts only). Tests: DoNotContactControllerTest, SmsGatewayTest, SmsKeywordServiceTest, **************** VoipRepositoryScanTest (RED on origin/main, GREEN here), VoipMessageServiceKeywordTest, InternalAuthFilterKamoaiTest.
· AIService
SIs hear with the best speech model the org enabled — with no order set, the best VERIFIED STT model ranks first by published accuracy (SttQualityRanking, provider-neutral; TTS keeps in-cluster first); call tickets take only a session dialect the client speaks; E4 forwards a transcription prompt as each vendor's vocabulary hint (prompt, keyterm/keywords, word_boost); Settings → SI routing shows the model an SI uses now, and for STT on calls and in meetings
· kamo-shared-library
The organization's do-not-contact list, per channel
One company-wide list per organization (owner 2026-10-15), scoped per channel: a CALL entry blocks calls only, SMS texts only, EMAIL email only. Package com.kamo.z.shared.comms.dnc: DO_NOT_CONTACT (unique ORG_ID, CHANNEL, ADDRESS), the append-only DO_NOT_CONTACT_EVENT audit trail and DO_NOT_CONTACT_SETTINGS (per-org call mode WARN | BLOCK, default WARN). DoNotContactList (plain class; each service declares the bean) is the one reader and writer: isBlocked(orgId, channel, address) / find / filterBlocked for every contact path, add (idempotent, first date kept), remove and removeById (a reason of 3+ characters is required, and the REMOVED event keeps it), search, history, activity and the call mode. Addresses are E.164 for CALL and SMS (PhoneE164, then E164's NANP reading) and lower-cased for EMAIL; an unreadable address is refused. DDL: voipservice **************** (applied 2026-10-15 04:35Z). Tests: DoNotContactListTest, ****************
· kamo-shared-library
AiCallEventType.TEAM_REQUEST records who asked for an SI's call and the message to deliver
Owner 2026-10-15: when a team member asks an SI to call someone, the SI calls them with no approval and no consent gate, says who it is calling for, delivers their message, and reports back. K6 records the request (requester, requestRef, the name given, the message) on this event; the call context and the follow-up read it. STRING-persisted, appended after CONSENT_BASIS.
· kamo-internal
SI routing shows "Phone calls and meetings" — the VOICE_CONVERSATION slot's own model order, with a hint that names its fallback order
Owner 2026-10-15: SI phone calls on their own model order, chat unchanged. routingApi mirrors shared-lib AiRoutingSlot (ROUTING_SLOTS) and the slot entry's kind and fallbackCapability. No home admin-bar link.
· kamo-internal
A member may let SI colleagues greet them with the short form on internal calls
SI call disclosure, owner decision 11 (2026-10-15): the SI colleagues card on the member's own profile (Work room) gets "Short greeting on calls with SI team members", a switch that is off unless the member turns it on. On, an SI they call from their desk phone greets them by first name ("Hi Sage, it's Luna, the automated assistant.") and still says the call is recorded; calls from outside the office phone system always get the full introduction. It saves with the card's whole set (siCallShortGreeting, SecurityService 259063b9); an answer from before the opt-in reads as off. An administrator sees it read-only. No home admin-bar link. A sixth palette row in the field registry.
· AIService
SI phone calls and meetings route on their own model order — kamo:voice[:light|medium|heavy] on E2 routes as chat in the VOICE_CONVERSATION slot, and Settings → SI routing lists, offers and saves the slot
Owner 2026-10-15: "Use gpt-6-sol for calls"; chat and everything else stay on their order. - ModelSelector: kamo:voice and kamo:voice:<quality> (chat only) are kamo:auto / kamo:<quality> in the slot. - AiRouter: in a slot, the slot's ranks first, then the capability's own ranks (offset below them); a slot with no models listed is today's order unchanged, so no org changes behaviour. The slot requires TOOL_CALLING, so an SI needs it VERIFIED. The GPT-6 tool fallback (reasoning_effort none) applies as before: no effort is set here. - Only E2 turns that send kamo:voice read the slot; a call's summary keeps its plain alias (test). - E13-E15: VOICE_CONVERSATION is listed right after TEXT_GENERATION (kind SLOT, fallbackCapability), its candidates are the TEXT_GENERATION models with TEXT_GENERATION and TOOL_CALLING VERIFIED, and only those can be saved. - E1 reads routingSlot, so a service can ask which model a call would get without placing one.
· kamo-shared-library
A routing slot for SI phone calls and meetings — **************** routes as TEXT_GENERATION with TOOL_CALLING required, and CapabilityRequest carries the slot (null for every existing call)
Owner 2026-10-15: SI phone calls get their own model order; chat and everything else stay as they are. A slot is not a model capability: its order is stored in ai_routing_preferences under its NAME (VARCHAR(64), no CHECK, so no DDL), and a slot name never equals an AiCapability code (test). A request naming the slot gets the slot's required capabilities in additionalCapabilities, so an SI caller needs them VERIFIED.
· SecurityService
A person may let SI colleagues greet them with the short form on internal calls
SI call disclosure, owner decision 11 (2026-10-15): the colleague short form ("Hi Sage, it's Luna, the automated assistant.") is used only for active staff who opted in on their own profile, on a call that stays on the internal phone system. The opt-in is **************** (CP12's row about being reached by SI colleagues): nullable, NULL and FALSE are off, hand-applied before this build **************** applied 2026-10-15T02:55:39Z). - CollabPreferencesWire: siCallShortGreeting, a boolean (null or absent = off), saved with the rest of the set by the member themself (403 NOT_SELF as before). - CollabPreferencesView: the GET/PUT answer carries it. - ColleagueCallCardService: the call card carries shortGreeting, which kamoai-service reads with intelligenceType and active.
· kamo-internal
A meeting in a message — add it once, and see what its organiser changed
The strip that said "Not on your calendar" and nothing else becomes the meeting's card: a date tile, the time in the member's zone with the organiser's own clock beside it when they differ, place, join link, repeat and guests. - A new invitation offers Add to calendar and Not now; adding says that later changes will apply on their own. - A follow-up to a meeting already on the calendar is applied as the message opens (EmailService has usually done it on arrival) and the card shows "Updated on your calendar" with each change, old value struck through beside the new one. - Cancellations, replies ("Dana accepted"), the member's own meetings, an invitation older than their calendar, and failures each say plainly what they are. The bell's CALENDAR_EVENT_CHANGED notice is written a line per change, so the toast, the notification centre and the detail dialog now keep the body's line breaks; a cancelled meeting gets the EventBusy icon.
· EmailService
A meeting that arrives by email asks once, then keeps itself current
A new invitation in the mail reader now offers "Add to calendar". Once a member has added it, the organiser's follow-ups apply on their own: a moved time, a new place, a cancellation reach the member's copy without asking, and the member gets a CALENDAR_EVENT_CHANGED notification saying which meeting changed and from what to what ("From: Wed, Oct 14 · 2:00 – 3:00 PM EDT / To: Thu, Oct 15 · 3:30 – 4:30 PM EDT"), in their own zone. Their own meetings are never moved by mail. - InboundInvitationService: member-scoped status, add and applyFollowUp, through IcsImportService.upsert (zones, all-day, series, edited and cancelled instances) with a before/after snapshot for the notice. Only a newer SEQUENCE applies; retried on a serialization conflict; the notice is raised after the commit and deduped per event, change and SEQUENCE. - On arrival: MessageIndexer hands every parsed KamoMail message to InvitationArrivals, so the calendar is current before the message is opened. Other mailboxes apply when the message is opened. - **************** add and apply re-read the meeting from the member's mailbox, never from the browser. - InvitationReader replaces the inbound parser, which read every TZID time as UTC (2:00 PM Eastern became 2:00 PM UTC) and could not read an all-day date. A message carrying its meeting only as an invite.ics attachment is now recognised; Microsoft 365 meeting messages are read from their MIME; an unreadable calendar part no longer fails the whole message.
· kamo-internal
Explain Confidential, Record a decision and Disclosure history to a first-time member, and give all three a premium dialog
Each tool now opens with a plain-words, three-step guide the first time a member uses it (remembered per member on the server via the UI-preference map, key featureIntrosSeen; shown when the preference cannot be read), folded afterwards under a "What is this?" button in a shared animated dialog shell: brand hero with a moving glyph, staggered sections, pinned actions, success moment, reduced-motion aware. - Confidential: the lock now opens a dialog with the guide, the current state (who marked it, when), what the mark does and does not do, and the one action; a member the marker rule refuses reads why instead of meeting a dead button. Refusal toasts, aria-pressed and one change in flight kept; the menu-row variant opens the same dialog. - Record a decision: kind cards with a line each, examples, a title counter, quick choices for when it applies (no end / today / this week / this month / pick dates, on the member's calendar), a live preview of the card, a refusal that scrolls into view, a success moment linking to Decisions; supersede shows what it replaces. Disclosure notice and Send anyway kept. - Disclosure history: overview tiles by decision and channel with a dot strip over time, a timeline view (the default on narrow screens) beside the table, a skeleton while loading, an empty state that says what would appear and why, and a plain forbidden card. Keyset Load more, zone toggle, 403 and retry kept. - Rich tooltips (name, state, one plain sentence) on the three toolbar buttons, on every surface that reuses them. - zIndex plumbing: the dialogs, the decision opener and the disclosure notice take a layer; ChatBox lifts them only when it paints maximized on its own (MAXIMIZED_Z) — embedded tool windows stay in the dock band under MUI.
· kamo-internal
Speak your message, hands-free conversation and read replies aloud in every chat window
The public web chat's voice — dictation, replies read aloud, and a hands-free loop — now runs in every chat-style tool window: member chat (including chats with SI members), support tickets, social conversations, Exec2Exec, the SI assistant and SMS. - @kamo/voice-core 0.1.0 is the state machine, shared with the widget; useChatVoice binds one controller per window (SSR-safe, created lazily) and hands it down by context. - The shared Composer (and the SI and SMS composers) draw the mic beside emoji/attach, a polite status line, and the window's Hands-free / Read replies aloud toggles (aria-pressed). - Replies reach the voice from each window's existing realtime handler: never a second socket subscription, never the app-wide chat:message-received nudge. SMS replays to a newly opened window are marked and not re-read; the SI chat's turn resolves with the answer itself. - A minimized window is silent; one voice app-wide (a newer listen/speak stops the last; a reply is not read over another window mid-conversation; the name pronouncer shares the rule). - Hands-free / read-aloud are member UI preferences, broadcast to open windows; hands-free comes back paused. Speech language follows the app locale.
· VOIPService
Ai-lines names the number a missed call can be returned to
Each line of /ai-lines?orgId= carries callbackNumber: the SI's own line, else a number assigned to it, else its phone server's main number, else the org's only active number; null when none (never a guess among several). The minute SIP sync (no orgId) skips the lookup.
· kamo-internal
The editor offers JOIN_MEETING, so an organization can set its own rule for an SI joining meetings
Owner 2026-10-14: joining one of the organization's meetings is its own action class (shared-lib 2d2f2efd), not PLACE_CALL. ACTION_CLASSES lists it after the two call classes, and APPROVAL holds one item more (22), as shared-lib's CharterSection does. The label comes from the charter vocabulary as for every class. Tests: sections.test.ts (the list, the count and APPROVAL's cap)
· APIService
Carry website visitors' Live Support files through the gateway
- POST **************** the multipart "files" parts are re-sent to MediaService as a fresh multipart body, streamed from the container's spooled copies (the generic proxy cannot forward multipart: Spring has already parsed it). More than 5 files, or one over 25 MB, is refused here without an upstream hop. - GET …/attachments/{objImgId} and …/preview: streamed byte-for-byte (an agent's file may be gigabytes; the generic proxy buffers in heap), Range forwarded, and only the file's own headers cross (type, length, range, disposition, nosniff, CSP, rendition). - CORS now exposes Content-Disposition, Content-Length, Content-Range, Accept-Ranges, X-Attachment-Rendition and Retry-After to the widget (Traefik's cors middleware sets no expose list of its own). - An upstream refusal keeps its Content-Type, so a JSON error reaches the widget as JSON. - Rate limiter: attachment routes are conversation traffic, not session creation (they were throttled at 3/min/IP); uploads 10/min/IP per conversation, downloads 120/min/IP per attachment.
· MediaService
Website visitors can attach files in Live Support — safe types only
A visitor on a lender's website can now send documents, images, zips and ordinary audio/video to the agent in the Live Support tab, and read back both their own files and the agent's. Scripts, autoruns and executables are refused. The gate is PublicVisitorAttachmentPolicy, judged against the bytes rather than the name or declared type: a default-deny extension allowlist (by the last extension, with dangerous inner extensions, bidi overrides and Windows trailing dots refused), magic bytes that must match the claimed family, PE/ELF/Mach-O refused whatever the name, text that must really be text, and ZIP-family files read through their central directory only (no decompression) — no executables, nested archives, symlinks or path traversal in a .zip; no macros, ActiveX or embedded OLE packages in Office/OpenDocument; no VBA project or packaged object in legacy OLE2; no embedded object in RTF. New endpoints (behind APIService's key/origin/rate gate, session-token authorized): - POST **************** (multipart "files", 1-5, 25 MB each) - GET|HEAD …/attachments/{objImgId} and …/preview (same safety headers as the member route, now shared through AttachmentResponses) - POST …/messages accepts "attachments": [{imgId, sortId}] — only the visitor's own uploads into this conversation; history rows carry attachments in the member shape. Visitor files are stored like any chat attachment (CHAT_ATTACHMENT, the session guid, created by the org's system member) so the agent's window renders them unchanged. sendSupportMessage now announces everything (NATS frame with attachments, activity with hasAttachments, pending count) after commit, and is @RetryOnDbConflict.
· KlusterServices
Postfix takes the name mail.kamouniverse.com once it resolves here
kamouniverse.com is the platform's sending domain, but the server still introduced itself as mail.kamocrm.com in its EHLO, Received headers and bounces. Postfix now picks its name at start: mail.kamouniverse.com when that name resolves to the same address as mail.kamocrm.com, otherwise mail.kamocrm.com as before, since a HELO name with no A record is itself a spam signal. smtp_helo_name follows myhostname. auto-cert now issues mail.kamouniverse.com and mirrors it into the mail namespace, restarting postfix. It can only issue once the A record resolves, so adding the record is all it takes for the switch: the restart picks the new name and its certificate.
· kamo-shared-library
JOIN_MEETING, joining one of the organization's meetings, is its own action class and not PLACE_CALL
Owner 2026-10-14: Luna (crm) asked the owner to approve joining the meeting he had just told her to join. meetings_join was classed PLACE_CALL, so the charter's "Placing phone calls: ask for approval first" applied to it (approvals 3f6fe9fc, 4ad68782). Joining a meeting is not placing a phone call: AiActionClass appends JOIN_MEETING (AUTONOMOUS by default, not monetary); the Tool Plane exempts it from the autonomy ceiling while no charter layer names it (mcpgatewayservice), and naming it in a layer is how an organization tightens it. CharterSection.APPROVAL holds one item more (22). Every resolved charter gains the class, so every charter stamp moves once (CharterStampTest's legacy golden moves with it, as SEND_INTERNAL_MESSAGE's and RELEASE_INFORMATION's did). Tests: CharterEnumsTest pins the order, the default and the cap. AiAutonomyModeTest's draft-kind pin fails on origin/main as well (RELEASE_PROPOSAL), untouched here
· kamo-internal
Typed remote-monitoring client and the pure progress, limit and refusal rules
· kamo-internal
Mirror the three remote-monitoring rights and gate the RPM_ALERT kind
· APIService
Serve Tesla's partner key path and exempt it from access rules (IoT SP08-D2)
GET **************** forwards sessionless (identity headers and Authorization dropped) to IoTService **************** the exact path (container- normalised) is exempt from the platform org's IP/geo rules.
· KlusterServices
Open TCP 8285 for Tuya's message service (IoT SP08 D4)
· MediaService
An SI's own join (M1) carries the meeting's dial-in, for when it cannot be put straight into the room
2026-10-14: SIs asked to join a meeting told people "there's no dial-in" and asked for another way in. kamoai-service now picks the way in itself: straight into the room (LiveKit -> meet-<room>) first, and the meeting's own dial-in only when that cannot be placed. For that it needs the dial-in, so M1's join names it - {extension, pin} - when the caller is the SI joining itself, the bridge is on, and the PIN resolves to this meeting now (DialInPins.resolve: a joinable Kamo Meet inside its PIN window), so a PIN handed out is one the bridge takes. A person inviting an SI is not given the PIN through M1. Tests: MeetAiControllerTest (the SI's own join carries it; a person's invitation, a PIN outside its window and a meeting without one carry none).
· kamo-shared-library
SP12 health enums, entities, repositories and monitoring-state derivation
· kamo-shared-library
SP12 RPM code rules, period math and threshold math
· kamo-shared-library
SP12 pure vitals mapping — LOINC/UCUM, unit conversion, plausibility, opaque Validic uid
· kamo-shared-library
SP12 Observation source columns, FHIR device and tag, remote-reading queries

Your universe is waiting.
Start free in minutes. Add products whenever you are ready — no migration, no new logins.
Free forever — no credit card required