Changelog
What's new.
Every improvement to the platform, as it ships.
· kamo-register
The sign-up screens credit KamoUniverse and fall back to its apex
kamouniverse.com is now the platform's primary apex (kamocrm.com stays a fully served legacy apex): - Theme.tsx: PLATFORM_APEX, the theme host used before the browser knows its host or on a host with no apex, is kamouniverse.com. Any real host still asks theme.<its own apex>. - RegisterFlow: the server-render placeholders for the sign-in link and the org domain are login.kamouniverse.com / kamouniverse.com. The browser replaces both with this site's own login.<apex> on mount, as before. - "Powered by / Built on / Running on" pill names KamoUniverse on every door without an apex brand of its own and links to www.kamouniverse.com (its business-OS page and product line stand in for kamocrm.com's /product and /features, which that site does not have). - ThemeLogo's default alt is a neutral "Logo" (it paints any org's logo). - The sorry page's "Contact Support" is support@kamouniverse.com: the mail stream verified kamouniverse.com inbound is live and aliased to the same mailbox. Kept: JAVA_API_BASE = api.kamocrm.com in the BFF routes. It is the shared platform API host (OrgApiHosts.PLATFORM_API_HOST stays) and a server-to-server call; api.kamouniverse.com routes identically, so moving it buys nothing. Cookie scoping and the checkout hand-off are host-derived and untouched.
· kamo-login
The sign-in screen credits KamoUniverse and falls back to its apex
kamouniverse.com is now the platform's primary apex (kamocrm.com stays a fully served legacy apex): - Theme.tsx: PLATFORM_APEX, the theme host used before the browser knows its host or on a host with no apex (localhost), is kamouniverse.com. A page browsed on any real host still asks theme.<its own apex>, so login.kamocrm.com keeps reading theme.kamocrm.com. - "Powered by / Built on / Running on" pill names KamoUniverse on every door without an apex brand of its own (white-labelled tenants and the legacy kamocrm.com door) and links to www.kamouniverse.com. That site has no /product or /features, so those variants go to its business-OS page and its product line (both verified 200 in every locale). The KamoUniverse door's own override is unchanged. - ThemeLogo's default alt is a neutral "Logo": the image is whichever organization's logo the screen paints, so "KamoCRM Logo" was wrong for every tenant. The theme folder the kamouniverse.com door paints, cookie scoping and the checkout hand-off are host-derived and untouched.
· kamo-internal
The All tab and badges reload when anyone writes a note on the lead
The lead's Communications opens on All, whose timeline reloaded only when this member posted a note from it, so a note written by someone else (another member, a Hot Reassign, an AI member's tool) still needed a page reload there. useLeadNoteWritten follows /topic/media/feed/<orgId> on the tab's shared media socket, learns the lead's stream from its feed, and calls back (coalesced, and after a reconnect) for a note in that stream; LeadCommunications reloads the timeline and the badge counts with it.
· kamo-internal
A lead's Notes reloads when a note is written in its own stream
MediaStream (a lead's Notes, the Clubhouse feed) listened on /topic/media/feed/<orgId> but reacted only to POST_CREATED/POST_UPDATED — and nothing was ever relayed there. MediaService now relays the org's media.feed events, where a new note or post is COMMENT_ADDED naming its stream. The feed remembers the stream it lists (the response's new streamGuid, or its posts' guid) and reloads, quietly, only for a note in that stream: a Hot Reassign note now appears for another viewer of the lead without a reload. The rule is app/lib/mediaFeedEvents.ts.
· SecurityService
A feed names the stream it lists (streamGuid)
The browser's feed reloads when /topic/media/feed/<orgId> says a note was written in its own stream. A lead with no notes yet has no post whose guid names that stream, so the first note another member wrote there (a Hot Reassign note, typically) could never appear live. GET /api/security/media/feed now returns streamGuid for the lead feed and the org feed.
· MediaService
Relay media.feed.<orgId> to /topic/media/feed/<orgId>, guarded by org
A new post or lead note is announced on media.feed.<orgId>, and kamo-internal's MediaStream (a lead's Notes, the Clubhouse feed) listens on /topic/media/feed/<orgId> — but nothing ever relayed one to the other, so a note one member wrote (Hot Reassign included) appeared for another viewer of the lead only after a reload. MediaFeedStompRelayController subscribes media.feed.* on core NATS with no queue group (every pod delivers to its own sockets) and relays only a numeric org id. StompDestinationAuthz now guards the topic like the social feed: SUBSCRIBE by the session's own org only, no client SEND.
· KlusterServices
Svc_media may subscribe media.feed.* to relay the org feed to the browser
SecurityService and MediaService announce a new post or lead note on media.feed.<orgId>, and the browser's feed (the lead view's notes, the Clubhouse feed) listens on /topic/media/feed/<orgId>. Nothing connected the two: svc_media could publish media.feed.> but not subscribe to it, so no relay could exist. MediaService's new MediaFeedStompRelayController subscribes media.feed.* (one token: the org id; media.feed.lead.* is not relayed).
· kamo-internal
AI invitees join by themselves; meeting history link; keep-AI-transcripts setting
The meeting dialog's invitee picker reads SecurityService's by-department answer under membersByDepartment (it read the top level, found nothing and never showed a picker when opened without a member list). An AI invitee wears the AI chip and brings the joins-by-itself hint from every entry point: the Interaction Center and member list pass the flag, and a member preselected from a conversation or member card is flagged from the picker's list. The invitation subject is sent as the meeting title. Meeting history is offered with VIEW_MEETINGS and goes through navIntent. Recording settings gain the AI companion transcript switch, outside the recording block; meet setting hints meet WCAG AA contrast.
· KlusterServices
Kamo Meet bridge — Jigasi transport and endpoint, PIN dial-in on 7800, trusted entry for AI members
No user- or caller-typed text enters a dialplan expression: the room checks compare lengths and the AI's display name is filtered before it is tested (a quote in a typed room name could otherwise turn the check false and dial the filtered room). The resolver header is set once, before the PIN retry loop.
· kamo-internal
The assistant streams its answers, with REST as the fallback
The assistant had been REST-only since before KamoAI: its WebSocket context reported "connected", opened no socket, and posted each message, so the whole answer arrived at once. It now opens AIService's /ws/ai-chat on api.<the org's domain> with a one-time key (/api/generate-otk?purpose=ws) and shows the answer as it is written: - TOKEN frames stream into the answer bubble; MESSAGE_COMPLETE's saved answer (with its citations) replaces it, and a frame without text keeps what streamed; - a TOOL_RESULT files the text before the tool call and the tool's row, as the transcript reads on reload; - messages go one at a time, so two answers never stream into one bubble; frames of another conversation on the same socket are ignored; - REST stays the fallback when the socket cannot be opened (no key, no api host, a refused or slow handshake); a socket that drops mid-answer ends it with an error instead of asking again; - the socket opens with the first message and closes after five idle minutes. The 501 /api/ai/ws placeholder, which said the socket could not be reached, is removed. SP99-T10.
· KlusterServices
A workstation probe that never prints the MCP token
SP11's acceptance read an AI workstation's descriptor and probed its MCP bridge by hand, and one diagnostic line printed a live bearer token into a session transcript (SP11 Task 30 review, finding 2). canary/workstation-probe.sh does both. describe prints the descriptor with the token as "<present>" or null (--refresh mints a new one); probe runs initialize, the tool names, a screenshot's type and a wrong token from an ephemeral container in the Tool Plane's pod, and every line it prints, its errors included, is masked. The token lives in a shell variable and the probe's stdin only. The tests fake kubectl with a known token and run the generated probe with curl against a local bridge.
· kamo-login
Send a universe sign-up to its checkout, or to create its organization
kamo-register parks a paid sign-up's plan in sp_checkout_plan, and the KamoUniverse site's plans are the universes' own (family_premium, community_starter, personal_pro, ...). planCheckoutUrl only honoured starter|business|enterprise, so a buyer who had just verified their email and signed in was dropped on the home screen instead of checkout. planCheckoutUrl moves to lib/planCheckout.ts and accepts the shared contract **************** It forwards the universe to **************** — always the PLAN's universe, since that is the ladder it is sold from — whenever the plan belongs to Family, Community or Personal or kamo-register parked sp_checkout_universe; it clears that cookie with the plan and code. A FREE sign-up into Family, Community or Personal parks a universe and no plan. With no explicit ?next= and no job advert, it now lands on /network/create?universe=<u> in the workspace, where the create page preselects that universe; the cookie is spent on use. Business is the create page's own default, so a Business sign-up lands wherever it always did. The Business funnel is unchanged: **************** still yields **************** with no universe on it. tests/planCheckout.test.mjs pins it (node --experimental-strip-types --test tests/*.test.mjs).
· kamo-register
Carry a universe plan and its universe into checkout
The KamoUniverse site signs people up as **************** and its paid plans are the universes' own: family_premium, community_starter, personal_pro and so on. The completion handler only parked starter|business|enterprise, so every universe sign-up that had chosen a paid plan finished registration and was never sent to checkout. The hand-off moves into lib/checkoutHandoff.ts and accepts the shared contract **************** (free plans need no checkout and park nothing). It also parks sp_checkout_universe from a valid ?universe= — with or without a plan, since a free sign-up into a universe still has to reach "create your organization" with that universe chosen, which kamo-login now does. Same cookie attributes and apex rule as before, so kamo-login clears what this writes. The Business funnel is byte-for-byte what it was: ?plan=starter|business| enterprise[&code=] writes exactly the same cookies. `npm run guard:checkout` pins that, and the rest of the contract. A sign-up that names a universe says so under the step counter: "You're starting a Family universe".
· kamo-internal
Ask which universe a customer's organization belongs to, and read plans by type
The platform now sells four plan ladders — Business, Family, Community and Personal — and an organization's universe decides which one it is offered. /network/create asks the question, once, where it belongs: only when the organization is being created directly under the platform (the session's organization is top-level), which is where a customer's first organization is made. Everywhere else the new organization is a branch of an existing customer and SecurityService gives it its parent's universe, so no picker is shown and no `universe` is sent. The parent is read from /api/org/current rather than the shell's OrgContext, which on the shared host starts as the platform and would flash the question at somebody creating a branch of their own company. The picker starts answered — Business, or whatever the KamoUniverse site's hand-off link named **************** — so it can never block a launch. A paid plan from that link that belongs to the chosen universe is carried through `/validate?next=` into the NEW organization's billing console with the plan already selected, keyed on the creator's own membership there. `universe` rides on POST /api/security/org only when it was asked; SecurityService parses that body field by field, so the key is ignored until the universe-aware SecurityService lands. The plan surfaces compared codes: `planCode === 'FREE'` (MyPlanWidget, PlanUpgradeDelta, PlanStandingHero, /myplan) and `=== 'ENTERPRISE'` (SubscriptionOrderWizard). On a Family organization there is no FREE — its floor is FAMILY_FREE — so the page found no floor to measure a trial against and offered the free plan as an upgrade. They now ask planLadder: the floor is plan_type FREE or a code that names it (every answer given before is kept), and a negotiated plan is ENTERPRISE or <UNIVERSE>_ENTERPRISE. The billing console was already catalog-driven and needed nothing. /myplan shows the organization's universe as a read-only chip on the org line. An absent `universe` — every organization created before universes — reads as Business (universeOrDefault). Keys: kamo-translation-dictionary 1fcb6274 (pushed first).
· APIService
The public subscription catalog takes ?universe=
The KamoUniverse site shows the Family, Community and Personal plan ladders, and the public catalog proxy only forwarded locale, so it could only ever return the business plans. **************** and **************** now forward an optional universe (letters only; anything else is a 400 before it reaches the upstream query string). SecurityService decides what it names and reads an absent one as business, so kamocrm.com, which sends none, gets the same upstream URL and the same four plans as before.
· BillingService
The billing console sells an organization its own universe's plans
KamoUniverse adds Family, Community and Personal ladders beside the business one, and BillingService assumed a single catalog: "the Free plan" was the code FREE, "negotiated" was the code ENTERPRISE, and the market was the root's first one. - resolveBillingMarketId: a Family/Community/Personal org buys from its universe market (KamoFamily, ...), so the console lists that ladder only. Business unchanged. - create/update refuse a plan from another universe than the target org's, so a family cannot post the KamoCRM market id and buy the business ladder. - Free-plan checks go by plan type (FAMILY_FREE, ...) or the business FREE code; an expired trial or a lapsed plan falls back to the org universe's Free plan. - Seat ceilings: business keeps "25 on any plan, Enterprise above"; the new ladders cap each plan at its own seat count and point at the next plan up, with only the negotiated plan uncapped (Personal has none: Pro's 5 is the end). - EnterprisePlanGuard guards every plan_type CUSTOM plan on the platform org, not only the one coded ENTERPRISE. - The shared add-ons (mail, phone, storage) filed under the KamoCRM market are listed on a universe market when compatible with its plans, and a business promotion is not advertised on a universe market. Plan DTOs carry "universe". Needs kamo-shared-library's UniversePlans (pushed first).
· SecurityService
Organizations are created in a KamoUniverse universe and bill from its catalog
KamoUniverse sells Business, Family, Community and Personal plan ladders from the platform org. SecurityService decides which ladder an organization is on, and every place that assumed "the Free plan" or "Enterprise" now asks per universe. - POST /api/security/org takes an optional "universe" (business | family | community | personal, case-insensitive; unknown is a 400). Absent, a branch inherits its parent's unless the parent is the platform or a platform product; otherwise BUSINESS (stored null, like every existing org). - Org payloads (/org/current, /org/{id}, /org/domain/{host}, /org/ref/{ref}) carry "universe" through the entity; the /network rows (my-networks, all-networks) carry it too. "BUSINESS" for every org that predates universes. - /pos/subscription-catalog and /pos/subscription-promotions take ?universe=. Absent is BUSINESS, so kamocrm.com and the public proxy see exactly today's catalog. List prices are withheld from every plan_type CUSTOM plan, not only the one coded ENTERPRISE. - EntitlementService: the Free floor is the member org universe's Free plan (a family's floor no longer carries the CRM); a trial grants the whole catalog for BUSINESS as before and the universe's top tier elsewhere. - BillingSetupService seeds a new org onto its universe's Free plan, in that universe's market (KamoFamily, ...), instead of FREE in KamoCRM. Needs kamo-shared-library's UniverseType / UniversePlans (pushed first).
· InitializerService
The Family, Community and Personal catalogs beside KamoCRM's business ladder
KamoUniverse sells four catalogs out of the platform org's one plan table. This lands the schema and the data for the three new ones. - UniverseColumnsMigration (@Order 0): orgs.universe_type and **************** nullable INT, no default - null is BUSINESS, so nothing is backfilled and no existing row changes meaning. - UniverseCatalogMigration (@Order 50), insert-if-missing and scoped by org_id: the KamoFamily / KamoCommunity / KamoPersonal SUBSCRIPTION markets; the twelve plans (FAMILY_*, COMMUNITY_*, PERSONAL_*) with universe_type and an explicit market_id, so KamoCRMMarketBackfillLoader never absorbs them into KamoCRM; their monthly/annual/triennial per-seat prices; USERS (seat ceiling, unlimited on the negotiated plans) and STORAGE (4 GB/seat) rows; the "Add-on" mail and phone rows; compatibility for the five add-ons every plan may buy (both mail, both phone, extra storage); and Community Premium's 51-250 seat band at 15% off. - UniversePackages: which apps each new plan includes, read by both **************** (the authority, which writes the rows) and **************** so they cannot disagree. No CRM on the Family or Personal ladders; mail, the origination systems, patient records and Hosted Computers on no universe tier. - The origination/EHR/Hosted Computers add-ons are made compatible with BUSINESS negotiated plans only when first created, never the Family/Community ones. - The totality test's "commerce children sit exactly with POS" rule is the business ladder's history; it now checks business codes, and a new check keeps every universe tier from bundling a commerce child without POS.
· kamo-shared-library
Organizations and plans belong to a KamoUniverse universe
KamoUniverse sells the platform to four audiences - Business, Family, Community, Personal - each on its own plan ladder with packages that fit it (a family gets no CRM). Nothing could tell those catalogs apart: every org resolved its Free floor from the root's plan coded FREE, which includes the CRM and applies whatever the org pays for, so a Family org would have held the CRM forever. - UniverseType (BUSINESS 1, FAMILY 2, COMMUNITY 3, PERSONAL 4) persisted by id through UniverseTypeConverter (unknown id reads null), JSON by name. Null means BUSINESS everywhere; business keeps being stored as null. - Organization.universe (orgs.universe_type) and SubscriptionPlan.universe **************** both nullable. Organization's getter never returns null, so every org payload says "universe":"BUSINESS" for the orgs that predate it. The columns were added and verified in Yugabyte by KamoInitializer's UniverseColumnsMigration before this was pushed. - UniversePlans: pure helpers (no new bean) for a universe's Free plan, top tier, negotiated plan and self-serve seat ceiling. - **************** the Free floor is the org universe's Free plan; a trial grants everything for BUSINESS (unchanged) and the universe's top tier elsewhere. - **************** universe) / getPromotions(..., universe): plans filtered to the universe (null = BUSINESS), add-ons/bundles/promotions pruned to the ones naming its plans; a row naming no plan is business-only. For BUSINESS the result is exactly the pre-universe catalog. Plan DTOs carry "universe".
· KlusterServices
Dev.kamouniverse.com forwards to www
The site is live on www.kamouniverse.com, so its pre-launch host stops serving it and 301s to https://www.kamouniverse.com with path and query kept, so existing links keep working. A redirectRegex middleware does it (kamo-nowww only canonicalises a bare apex); priority 300 outranks the site's own IngressRoute, which drops dev in a follow-up. The dev cert stays in STATIC_DOMAINS so https links still forward.
· kamo-register
Register.kamouniverse.com wears the KamoUniverse brand
register.kamouniverse.com is the platform's KamoUniverse sign-up door. Its host resolves to the platform organization, so the by-host lookup would answer "crm" and paint KamoCRM's logo, colours, title and favicon over it. A platform apex with a brand of its own now names its theme folder directly: kamouniverse.com -> public/kamouniverse/ (reserved as an alias in SecurityService, provisioned in the theme bucket). getThemePath answers it synchronously so the first paint is already right, and the host lookup is skipped there. A ?org= still paints that organization's own branding. The "Powered by" pill names and links KamoUniverse on that apex, and analytics counts register.kamouniverse.com beside register.kamocrm.com (platform doors only; the hostname keeps them separable). Same change as kamo-login's.
· kamo-login
Login.kamouniverse.com wears the KamoUniverse brand
login.kamouniverse.com is the platform's KamoUniverse sign-in door. Its host resolves to the platform organization, so the by-host lookup would answer "crm" and paint KamoCRM's logo, colours, title and favicon over it. A platform apex with a brand of its own now names its theme folder directly: kamouniverse.com -> public/kamouniverse/ (reserved as an alias in SecurityService, provisioned in the theme bucket). getThemePath answers it synchronously so the first paint is already right, and the host lookup is skipped there. A ?org= still paints that organization's own branding, on this door as on any other. The "Powered by" pill names and links KamoUniverse on that apex, and analytics counts login.kamouniverse.com beside login.kamocrm.com (platform doors only; the hostname keeps them separable).
· SecurityService
Kamouniverse.com is a platform apex, not a tenant domain
login.kamouniverse.com and register.kamouniverse.com become the platform's KamoUniverse sign-in and sign-up doors, backed by a root org_domains row on the platform organization beside kamocrm.com. Judged against kamocrm.com alone, that host resolves to the platform organization and takes the HOST-SCOPED path: only the platform's own members could sign in there, ?org= was ignored, and a member of any other workspace was refused. PlatformApexes holds the list (kamo.platform.apexes, default kamocrm.com,kamouniverse.com; kamo.platform.apex stays the PRIMARY): - isPlatformHost: every platform apex offers the workspace picker. - The fallback workspace host for a domainless organization is internal.<primary apex> whichever door the password was typed at — on the password path, the picker's /session/select, and after a second factor. The platform organization itself still lands on internal.kamocrm.com (OrgDomains.preferred: kamocrm.com sorts first). - OrgNetworkEnterPolicy skips platform apex rows, so a platform root row awaiting certificates can no longer mark the platform "setup incomplete" by winning the unordered first-root-row race. - DomainController's Kamo-owned-zone special cases and the SPF / DKIM / DMARC advisories recognise *.kamouniverse.com as they do *.kamocrm.com. - "kamouniverse" is a reserved alias: it is the theme folder those two doors paint, and an organization holding it would rewrite that screen with its first branding save.
· kamo-marketing
Let visitors talk to the chat — allow the mic and turn on hands-free
The hosted chat widget (1.3.1) can take spoken questions and read its answers aloud. Two things stood between it and www.kamocrm.com: - Permissions-Policy said microphone=(), which denies the mic to every frame including this document, where <kamo-chat> runs; the widget detects that and hides its mic button entirely. It is now microphone=(self): this origin only, embedded third-party frames stay blocked, and the browser still asks the visitor before any audio is captured. - The embed now passes voice="hands-free" (mic in the composer, a hands-free conversation toggle and read-aloud), plus the page's locale, so a visitor on /es is listened to and answered in Spanish rather than in the org default.
· KlusterServices
Bring www.kamouniverse.com live; apex goes through kamo-nowww
KamoUniverse is now the primary marketing site. www.kamouniverse.com stops 301ing to www.kamocrm.com and is served by the kamouniverse-marketing app (its own IngressRoute, Host(www.kamouniverse.com) at priority 100 — above kamo-marketing's ^www[.] catch-all). The bare apex now routes to kamo-nowww, which 308s it to https://www.kamouniverse.com and passes ACME HTTP-01 challenges through, exactly how kamocrm.com reaches www.kamocrm.com; the http route keeps excluding /.well-known/acme-challenge/ so cert-manager's solver still answers renewals. The kamouniverse-to-kamocrm redirect middleware is gone from the file; `kubectl apply -f traefik/routes/` doesn't prune, so it is deleted from the cluster by hand after this deploys. The four audience domains already forward to www.kamouniverse.com, so they now land on the site in one hop instead of two.
· KlusterServices
Certificates for every platform host on kamouniverse.com
login.kamouniverse.com and register.kamouniverse.com become the platform's KamoUniverse sign-in and sign-up doors, and from them a visitor reaches theme., capcha., api., internal. and the rest. None of those hosts has a certificate today, so Traefik answers them with the kamocrm.com default and the browser refuses the page. theme., capcha. and media. are subresource-only and cannot bootstrap their own certificate through the access-log scan, and the platform's kamouniverse.com org_domains row may only carry ssl_confirmed once every KnownAliases host is certified, so all 13 labels are listed rather than waiting on traffic.
· kamo-shared-library
Meeting calls in AI_CALL telemetry — kind, meeting id, REMOVED, meeting events
AiCallKind {PHONE, MEETING} and AI_CALL.KIND / MEETING_ID (SP14 DDL **************** AiCallOutcome.REMOVED, the four meeting event types, and the member+meeting live-call finder. SP13's AiCallEntitiesTest now lists the two SP14 columns and the new finder.
· KlusterServices
Run IBM Granite 4.0 H-Tiny in place of qwen3:4b, on 16 cores
The owner asked for a local model that runs substantially better on k3m1's AVX2-only Xeons (2026-09-27, ruling SP01-LLM-granite4). granite4:tiny-h is a hybrid Mamba-2/transformer MoE (7B total, ~1B active): several times qwen3:4b's prompt and answer speed on CPU, native tool calling, and no thinking pass. - postStart pulls granite4:tiny-h; - CPU limit 8 -> 16 (LLAMA_ARG_THREADS follows it), request 2 -> 4; - OLLAMA_CONTEXT_LENGTH 8192 -> 16384 (agent steps run 3-5k tokens); - OLLAMA_FLASH_ATTENTION=1. Memory stays 8Gi.
· kamo-internal
Explain AI activity on the lead header (SP10)
· kamo-internal
The AI room — workbench, autonomy dial, runs and rehearsal (SP10)

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