Changelog
What's new.
Every improvement to the platform, as it ships.
· SecurityService
Start a coder session in the cloud, and say which terminals are
repository as cwd) and answers `cloud: true`; the machine builds the a cloud session can start from. The terminal listing carries the agent's `cloud` mark through, for the cloud on the window's icon.
· KlusterServices
Start a coder session in the cloud from a local checkout
repository. The bundle is forced because our remotes are Forgejo, not GitHub, and the point is the checkout on this disk. No --permission-mode (the CLI drops bypassPermissions for a cloud session) and no --session-id or receipt (the cloud names the session and keeps no transcript here). The tmux session is marked @kamo_cloud so the terminal listing reports `cloud: true` and the console can put a cloud on the window's icon after ~/Kamo/Projects, most recently worked in first, for the menu to pick from.
· kamo-internal
Attachments sit above the body, as the files themselves
An email whose only content was a PDF had no way to open or download the file: the paperclip showed on the list row, and the reading pane showed nothing. EmailService had marked the PDF inline because the Gmail app gave it a Content-ID (fixed there in 0de0823); the pane dropped every inline part without asking whether the body actually painted it. The rule now lives in one place, listedAttachments: a part stays out of the list only when the HTML body references its cid:. The reading pane, the lead timeline and forwarding all use it, so a forwarded message no longer silently drops that file either. It does not trust the server's flag, which bodies cached in the browser still carry from before the fix. The sanitizer resolves cid: references with the same normalization, so anything left out of the list is always something the body shows. The attachment shelf, first thing under the header: - each file is a card showing the file itself: page one of a PDF (with its page count), the photo, or the type's glyph on blank paper - one click previews in a full-screen viewer: PDF through pdf.js with zoom and a page counter, images fit-to-screen with click-to-zoom, video and audio players, text, and CSV/TSV as a table; the arrow keys and a filmstrip move between files - downloads go through fetch, carrying the per-tab session header a bare <a download> never could, with a progress ring and a drawn tick - "Download all" hands every file over as one .zip written in the browser - keyboard-first, reduced motion honoured, axe clean; two cards per row in a narrow pane
· kamo-shared-library
Arive as a second external LOS, and a provider seam that is real
Arive is reachable only through its invite-only Zapier app, so the connection is Zapier in both directions: Kamo posts commands (create loan, update loan stage, update lead, convert lead) to per-market Catch Hooks, and Arive's triggers post back to a per-connection inbound token. The adapter, field mapper (three Arive namings, read from the live app), stage map, encrypted secrets and the activity log live in integrations.arive / integrations.los. LeadService no longer runs MeridianLink code for every link. Pushes go to the link's own LosProviderAdapter; a provider without one is skipped and logged instead of receiving a SOAP envelope. Inbound applies take a provider-neutral LosLeadPatch (contact, mailing address, mortgage facts, status) and attribute history to the provider. The resolver looks up member credentials by the connection's provider, and a market save with an unknown MLOS value is a 400 instead of a silent NO_MLOS. The columns this maps already exist in Yugabyte (LosAriveSchemaMigration's statements were applied before this push).
· kamo-internal
A designed launch screen, in the organization's own brand
Opening a computer showed one line of plain text on a white page, written straight into the tab with `document.body.textContent`. For up to ninety seconds — a resume reads the whole guest memory image back off a replicated volume — that sentence was the entire experience, and it reads as a browser error rather than as the member's own software. A page that looks broken is one people close at forty seconds, which cancels nothing. It is a screen now, built to this app's canon rather than invented: the radial primary/secondary wash on white, a white card at radius 14 with --elev-hover, the 90deg hero gradient as its rule, --ease-house, light mode. The organization's logo and its live **************** a ring counting down to the estimate the SERVER gave rather than a guess, and ambient washes that prefers-reduced-motion turns off entirely. A failure now has a way out of it. It shows the server's own sentence — "no node currently has 8 GiB free" is worth reading — and a Try again that re-runs the launch in the same tab. THE TAB KEEPS THE ORGANIZATION'S ICON, which is the other half of this. Navigating to the gateway hands the whole document to Guacamole, whose own `<link rel="icon" href="images/logo-64.png">` then owns the tab: a member opening their company's computer watched it turn into Guacamole's logo. The session is framed inside this page instead, so the icon and the title stay the org's and the hand-over is a fade rather than a white flash. Guacamole sends neither X-Frame-Options nor a frame-ancestors policy — checked, not assumed — and if the frame cannot be created the tab navigates the way it always did, because a prettier tab is not worth a member not reaching their computer. The icons are not rebuilt from a path: they are the `<link>` elements DynamicMetaLoader has already resolved for this organization, cloned. Re-deriving which of the two favicon directories an org uses would be a second answer that can drift from the first. Every string is a key, pushed to the dictionary first. The copy is injected rather than looked up — the page is written into another window by a plain function, where `useTranslations` cannot run — and there is deliberately no English default, so a caller that forgets fails to compile instead of shipping English to every locale. Thirteen tests, including that a server message is escaped rather than written as markup.
· kamo-login
Point a member who typed their work email at their personal one
Forgot-password resets go to the address on the user account, usually a personal one, but members type the work address their organization gave them. SecurityService now recognises a work address and answers 409 WORK_EMAIL with personalEmailHint, the account address masked. Nothing is sent. The wizard shows that in place of "check your inbox": the address they typed, the masked hint on a line of its own, and the field cleared, relabelled "Personal email address", focused, with the hint as its placeholder. The *** stays solved, since the member is being helped along rather than refused. Entering the personal address carries on to the code step as before. /recover's email step shows the same answer as an info message instead of "Failed to send recovery email".
· SecurityService
A work address on forgot-password asks for the account's own, shown masked
Members open their account with a personal address (gmail, yahoo) and are later given a work address by their organization, which is the one they remember and type on forgot-password. That only ever resolved on their own org's login host, and then the letter went to the work inbox. For 20 of KamoCRM's members that is a KamoMail mailbox, which is read inside the product they have just been locked out of. On any other host the address matched nobody, and the page said "if this email is registered, a link has been sent" having sent nothing. /api/recover/email/initiate now answers the account e-mail exactly as before. For any other address it looks for a member work address: first in the org whose login page this is (Primary mailbox, then members.email), then in any live org. When that names an account with an address of its own, nothing is sent. The answer is a 409 with reason WORK_EMAIL and personalEmailHint, the account address with every character of the name between the first and the last starred, and the domain in full. It is a 409 on purpose: a login page that predates the hint shows `message` for a non-2xx, and `message` says the same thing in words. An account with no address of its own still gets the letter at the work inbox on its org's host, since that is the only inbox on record. The hint says an address belongs to somebody, which the generic answer never did, so each one is counted by CodeAttemptLimiter: five an hour per address typed, plus the caller's address limit shared with the code checks. Past that the request gets a 429.
· EmailService
Move system mail and the platform org's own send domain to kamouniverse.com
platform.mail.domain now defaults to kamouniverse.com (config value, no code default change) and gates every unverified-domain org's transactional fallback From, as before. The platform org (kamocrm) used to bypass that property and send as its own verified OrgDomain instead - now PlatformOrgResolver forces it through the same fallback, so both follow one property instead of racing two different rows. **************** gets the matching piece for members: the platform org's primary mailbox gets a synthetic kamouniverse.com entry prepended (same mailbox, klusterservices mail/ aliases it back to the real kamocrm.com maildir) as the composer's new default, while the original kamocrm.com address stays fully sendable - additive, not a replacement. Every other org is unaffected either way. Bulk/campaign mail (bulk-email.platform-relay.*, hello.kamocrm.com) is a separate code path and untouched.
· KlusterServices
Add kamouniverse.com as a second regular-mail domain
Adds kamouniverse.com to postfix's virtual_alias_domains with a local-part-preserving regexp so any address on it lands in the identical kamocrm.com mailbox (no per-user setup, works for mailboxes created later too), and to relay_sender_domains so members/services can send as kamouniverse.com through this relay. rspamd gets its own DKIM keypair and signing block for the domain (selector mail, published as **************** hello.kamocrm.com / postfix-bulk are untouched.
· KlusterServices
Forward kamouniverse.com to kamocrm.com
DNS for the apex and www now point at k1m1's public IP. Add a redirectRegex route on both entrypoints (301, path/query preserved, ACME challenge path excluded on :80) and seed both hosts into auto-cert's STATIC_DOMAINS so they get certs despite not being a registered org domain, same as sign.pink.
· kamo-internal
Select-all/deselect-all for the states lead filter, default to all
The states dimension of the campaign audience panel's and drip trigger's lead filter showed nothing checked by default, even though an unset list already means "every state" on the wire — the same "no restriction" convention market/vendor/vendor-product already use. That made the picker look like it was filtering to zero leads rather than to everyone, and gave no quick way to start from "every state" or "none" before hand-picking a few. leadAudienceFilter.ts gains effectiveStates() (what to show checked: the stored list, or every state when it's unset) and withStates() (what to store back: the full set collapses to undefined rather than an explicit fifty-code list, so a lead with a blank state on file still matches "every state" the way an actual absence of a filter does — an explicit IN-list would not). Both panels get a "Select all" / "Deselect all" pair next to the states picker only; the other three dimensions are unchanged, since their empty-by-default framing was already correct.
· kamo-internal
DKIM and DMARC steps, so email we send for an org is fully authenticated
Two new steps after the existing SPF one, written for a non-technical owner the same way SpfCard already is — plain language, a "why this matters" box, and never asking for a change we don't actually need: - DkimCard: SecurityService generates a signing key for the domain the first time this loads, then reads whether the TXT record is published. Only two real states (nothing published yet, or something published that doesn't match) since a DKIM record can never collide with anything the customer already has — unlike SPF, there's no "add vs. edit" distinction to make. - DmarcCard: read-only. Because Kamo's mail always uses the org's domain as both envelope sender and visible From, DMARC's alignment check is already satisfied once SPF is done, whatever policy the org has chosen — so this only shows what's published (or suggests a starting point), never asks for a change. Both new **************** proxies follow the existing spf/route.ts exactly — no catch-all here, so a path without its own route file 404s before reaching SecurityService. DnsSetupStudio now shows 6 steps instead of 4; step numbering throughout updated for both the ownership-token and no-ownership-token cases. Depends on kamo-translation-dictionary 2f143f94 (the setup.dns.studio.dkim.* and setup.dns.studio.dmarc.* copy) and securityservice's advisory endpoints.
· EmailService
DKIM-sign transactional mail sent as an org's own domain
Once SecurityService confirms a domain's DKIM record is published (org_domains.dkim_verified), password resets, invitations and other transactional letters sent as NoReply@{org domain} now carry a DKIM signature alongside the existing SPF authorization — reusing the same DkimSigner campaign mail is already signed with, unmodified. EmailSmtpService gets a new send(...) overload taking a MessageSigner, mirroring sendVia's signed path: saveChanges() once, sign, then a manually-managed Transport, since the static Transport.send() used by every other send() calls saveChanges() again internally and would mint a new Message-ID after the signature was computed over the old one. The existing 6-arg send(...) now delegates to it with a null signer, so every other caller is byte-for-byte unchanged. **************** gates on dkimVerified specifically (not just a key existing) and only ever applies when we are already sending as the org's own domain — never under the platform's domain, and never on the platform-address retry when the org's own send fails. Depends on kamo-shared-library 5de57705 and securityservice's DKIM advisory endpoint, which generates and verifies the key this reads.
· SecurityService
DKIM and DMARC advisory endpoints for /setup/dns
Adds two endpoints alongside the existing SPF one, so an org's transactional mail (password resets, invitations, notifications) can carry a DKIM signature and the owner can see their DMARC posture: - GET /api/security/domains/{id}/dkim — generates a domain's DKIM key the first time it is asked for (never regenerated afterward, since swapping the key would break verification of anything already signed with the old one), checks whether the TXT record is published, and stores the verdict on **************** the same checked-state shape spf_verified already uses. - GET **************** — read-only. Every message Kamo sends as NoReply@{domain} uses that domain as both envelope sender and visible From, so DMARC's alignment check is already satisfied once SPF is done, whatever policy the customer has chosen. This only reports what is published (or suggests a starting record when nothing is) — it never asks for a change, since there is never one Kamo needs. **************** mirror SpfAdvisoryService's shape (a pure inspect(), a createLookup() seam for tests) and DkimAuthorizationService mirrors SpfAuthorizationService (persists the verdict, leaves it alone on a failed lookup). DkimDomainRepository is a plain JpaRepository the same way SpfDomainRepository is, for the same reason: it ships with one redeploy rather than a shared-library version bump. Depends on kamo-shared-library 5de57705 (OrgDomain's new dkim_* fields).
· kamo-shared-library
DKIM key fields for an org domain's transactional mail
Adds **************** to org_domains, mirroring the spf_verified/spf_checked_at pair already there — so a domain can hold a generated DKIM keypair and whether its TXT record has been confirmed, the same checked-state shape SPF already uses.
· EmailService
Narrow a campaign's all-leads audience and drip triggers by market, vendor, product and state
The campaign editor's "All Leads" box and the drip trigger's lead filter could only match every lead the sender can see, with no way to target a market, vendor, vendor product or state - the /leads grid has had all four for weeks. - New LeadAudienceFilter **************** threaded through CampaignAudienceInput -> AudienceDefinition -> AudienceQueries.shape()'s LEADS case, mirroring LeadGridSpecifications' predicates (market owns leads directly or via a vendor product; state is mortgage-aware property vs mailing, changed scope does. /estimate and /browse take the same four filters as comma-separated query params, so the editor's live count reflects an unsaved filter too. - TriggerConfig gains a states field alongside the existing marketIds/vendorIds/ productIds; DripTriggerMatcher.leadFilter() checks it. The event-payload path never carried vendor or state, so state is resolved live per lead (DripContacts. stateOfLead), the same way vendor already was from the vendor product. Every existing 3/6/8-arg call site keeps compiling unchanged via added overloads and secondary constructors; only AudienceGatherer, which always threads a definition's filter through, needed its call sites updated.
· kamo-internal
**************** filters on campaign leads and drip triggers
The campaign editor's "All leads" audience source had no way to narrow who it reached beyond visibility scope. Adds a filter panel (Market -> Vendor -> Vendor Product cascade, plus an independent State dimension) that narrows the live count, the browse preview and the saved audience, threaded through campaignAudienceApi's estimate/browse calls. The drip trigger's lead filter already had the Market/Vendor/Product cascade; adds the same State dimension there, alongside the existing three, non-cascading.
· KlusterServices
An org's own computers host gets a certificate and a route
computers is a standard alias now — every white-label org is told to point one at Kamo on /setup/dns, the way it points meet and sign — so two things had to follow it or the record would resolve and do nothing. A CERTIFICATE. computers joins SUBDOMAINS, which is the per-org list this service issues from. Without it an org's host has DNS and no TLS, which is a browser warning on their brand rather than a working desk. A ROUTE. The gateway matched Host(`computers.kamocrm.com`) and nothing else, so an org could add the record, watch the setup studio turn green and still land somewhere that is not this gateway. The new routes use HostRegexp(`^computers[.].+$`), the same shape **************** and the stream./grafana. routes already use. SEPARATE IngressRoutes rather than widening the existing rules, and the reason is TLS. The platform routes carry an explicit secretName, which is the only certificate that can be named ahead of time; a tenant's arrives when their domain goes live. Widening those rules would have made one named secret answer for every tenant domain — a certificate warning on somebody else's brand, which is worse than no route at all because it looks like Kamo got their domain wrong. The tenant routes leave TLS to Traefik's store and SNI, as meet does, and sit at lower priority so computers.kamocrm.com keeps its own. Serving every org from one regex hands nobody anything: the connection is chosen by the signed token, never by the hostname, and a token mints one computer for one member for one session.
· kamo-internal
The computers record on the DNS studio
One more card on /setup/dns, so an org configuring its domain is told about the host its Hosted Computers open on. Without it an org could follow the page to the letter, finish with every card green, and still have a computer that produces a browser tab which opens and closes — the link is built from computers.<domain>, and a link to a host with no record fails before any request leaves the machine. Required like every other label rather than only for orgs holding the app. The record is what makes the app work the day it is switched on, and an org sent back to its registrar months later for one record is an org whose first computer does not open. The label has to match KnownAliases in securityservice exactly — DomainAliasContractTest pins them, because a label the studio requires but verify-dns never emits a status for does not report as missing: the lookup miss falls through to "pending" and the card sits at "awaiting DNS" forever, whatever the customer's zone says.
· SecurityService
Computers is a standard alias, and the link uses the org's own
Every white-label org points a fixed set of labels at Kamo and computers was not one of them, so an org could follow /setup/dns to the letter and still have a Hosted Computer nobody could open. The link is built from that host; a link to a host with no record is a browser tab that opens and closes before any request leaves the machine, with nothing logged anywhere in Kamo and the computer itself healthy and awake throughout. Four lists move together or not at all. DomainAliasContractTest asserts exact equality between KnownAliases, SSL_PROBE_LABELS and its own mirror of the studio catalog, because the failure they guard against is silent: a label the studio requires but verify-dns never emits a status for falls through to "pending", so the card sits at "awaiting DNS" forever no matter what the customer's zone actually says. That is what happened to `media` for four months in 2026. Required like every other label rather than only for orgs holding the app. The record is what makes Hosted Computers work the day it is switched on, and an org sent back to its registrar months later, for one record, is an org whose first computer does not open. THE LINK NOW USES THE ORG'S OWN HOST buildLink takes the organization and builds https://computers.<its live domain>, via OrgLinkHosts.liveRootDomain — the same helper the borrower portal uses, and for the same reason: a domain that is configured but not yet verified would send a member to a host that does not resolve. Unlike the borrower portal, falling back to the platform host is CORRECT here rather than a broken link. One gateway serves every tenant and the machine is chosen by the signed token, never by the hostname, so an org with no live domain still gets a working computer — it simply opens on computers.kamocrm.com until its own domain is live.
· SecurityService
List only the Getting Started steps an organization is asked
An organization whose security model fixes every Compliance answer (no product a member may switch, one age range for each member type) was still counted against nine steps. It had to "save" an empty ninth step before its wizard latched as finished (reported 2026-09-21, Gnosia). OrgSetupStepApplicability decides which steps apply, from the applied model. Today only Compliance can drop out. The summary's steps, stepsCompleted and totalSteps, the completedAt latch and lastStep all run over that list. A step recorded before it stopped applying stays stored but isn't counted, so it comes back ticked if the model ever offers a choice there again. If the model can't be read, the step is shown: a shown step can be skipped, a hidden one can't be reached. The rule mirrors kamo-internal's getComplianceStepUi (what the step draws); both are pinned by the same case table. AppliedModelController now reads product policies and age ranges through the same helpers, so the endpoint and the rule can't drift apart.
· kamo-internal
A step the organization is not asked is not shown or counted
An organization whose security model fixes every Compliance answer (no product a member may switch, one age range for each member type) was still shown a ninth step saying there was nothing to configure. It had to "save" that step before the wizard counted as finished (reported 2026-09-21). SecurityService now lists only the steps an organization is asked, and the page follows that list: the rail, the "x of y" count, where the wizard resumes, what comes after the last step, and a `?step=` naming a step that isn't listed. A payload with no usable list still reads as every step, so a malformed answer never leaves an empty wizard. complianceStepApplies.test.ts pins getComplianceStepUi to the same case table as SecurityService's OrgSetupStepApplicabilityTest. The server decides whether the step is listed, and this function decides what it draws, so the two must agree.
· KlusterServices
Issue and mirror the certificate for computers.kamocrm.com
Opening a Hosted Computer produced a browser tab that appeared and vanished. The launch link is correct and so is the token: SecurityService builds **************** from KAMO_HC_GUAC_BASE_URL, whose default is that host. The host has no certificate and no A record, so the tab dies before anything in Kamo hears about it — no request reaches Traefik, nothing is logged, and the computer sits AWAKE and healthy the whole time. The hc-https IngressRoute already references tls-computers-kamocrm-com by name; the secret was simply never minted, because this service issues for an explicit list and computers.kamocrm.com was not on it. desktop.kamocrm.com is listed for exactly the same reason — nothing hits either host until somebody is handed a link, so neither is ever discovered from Traefik's logs the way a tenant domain is. Also mirrored into `hosted-computers`, because the IngressRoute lives there and references the secret by name in its own namespace. This mints NOTHING until the A record exists. The loop skips a host whose DNS does not resolve, because an HTTP-01 challenge against it cannot be answered: ⏭️ Skipping media.b11capital.com (DNS does not resolve; HTTP-01 would fail) computers.kamocrm.com needs the same A record as kamocrm.com and desktop.kamocrm.com, 47.181.8.84. That part is at the DNS provider, not here. Once it resolves, this issues within a loop and mirrors the secret without further action.
· kamo-internal
The message list names a lead or contact, not their address
A row was headed by the sender's display name, or the part of the address before the @ when there was none, so a lead who wrote in read as "jlrivera1984" above "jlrivera1984@mail.example". The header chip already preferred our own record's name, but the list never asked. VirtualMessageList now resolves each loaded page's correspondents alongside its avatars (one batched request, cached per address for the session) and heads the row with the name on record: a colleague, a saved contact, or - new with EmailService d3d6e03 - a lead. The monogram spells the same name. The correspondent card labels a lead "Lead" and offers "View lead".
· kamo-internal
The message list names a lead or contact, not their address
A row was headed by the sender's display name, or the part of the address before the @ when there was none, so a lead who wrote in read as "rodarisdolgovanton" above "rodarisdolgovanton@gmail.com". The header chip already preferred our own record's name, but the list never asked. VirtualMessageList now resolves each loaded page's correspondents alongside its avatars (one batched request, cached per address for the session) and heads the row with the name on record: a colleague, a saved contact, or - new with EmailService d3d6e03 - a lead. The monogram spells the same name. The correspondent card labels a lead "Lead" and offers "View lead".
· EmailService
A lead who writes in is named by the correspondent resolver
The mailbox list headed a lead's message with the part of their address before the @ ("jlrivera1984") when their mail client sent no display name, although the org holds them as a lead ("Jordan Rivera"). The resolver knew colleagues and saved contacts only. CorrespondentResolver gains a fourth tier, LEAD (after CONTACT), with the lead's name, company and leadId (a string: uids pass MAX_SAFE_INTEGER). LeadCorrespondents answers only what the leads grid would show the reader: VIEW_LEADS required, an unassigned lead needs VIEW_UNASSIGNED_LEADS (fails closed on free-for-all products), no phones (contact info is floored by rights this card does not check), and every lead named is written to phi_access_log as a LIST read in a PHI org, via AudiencePhiAudit. leads had no index on the address: the lookup walked all ~790k of the reporter's org's leads in 9.8 s. ix_leads_org_email_lower on (org_id HASH, lower(trim(email))) is in db/leads-email-lookup-index.sql and was APPLIED BY HAND as the owner before this deploy; the generic plan now takes 3.4 ms.
· EmailService
A lead who writes in is named by the correspondent resolver
The mailbox list headed a lead's message with the part of their address before the @ ("rodarisdolgovanton") when their mail client sent no display name, although the org holds them as a lead ("Anton Dolgov"). The resolver knew colleagues and saved contacts only. CorrespondentResolver gains a fourth tier, LEAD (after CONTACT), with the lead's name, company and leadId (a string: uids pass MAX_SAFE_INTEGER). LeadCorrespondents answers only what the leads grid would show the reader: VIEW_LEADS required, an unassigned lead needs VIEW_UNASSIGNED_LEADS (fails closed on free-for-all products), no phones (contact info is floored by rights this card does not check), and every lead named is written to phi_access_log as a LIST read in a PHI org, via AudiencePhiAudit. leads had no index on the address: the lookup walked all ~790k of the reporter's org's leads in 9.8 s. ix_leads_org_email_lower on (org_id HASH, lower(trim(email))) is in db/leads-email-lookup-index.sql and was APPLIED BY HAND as the owner before this deploy; the generic plan now takes 3.4 ms.
· kamo-internal
The member who caused a notification opens their card from it
"Dana Reyes dealt you in", "Weekly sync · Dana Reyes", an unread-chat card whose body is the sender: EmailService (29cb21e) now sends actorMemberId (as text) and actorName, the name exactly as the title or body spells it, and the bell draws that stretch of text as the member's name. - NotificationText cuts the name out of the title (else the body) once, at its first occurrence; everything else reads exactly as before. Used by the toast, the history row (clickToggles={false}: the row opens the notification) and the detail dialog. - A toast's card opens at the deck's level. MemberHoverCard/MemberName take zIndex, and the toast passes NOTIFICATION_DECK_Z (1500, now one constant with the deck's own). At the card's 1305 it opened underneath the deck it came from; at the same level it paints over it (it is appended to <body> later) and still under its own tooltips (appended later still). - UnreadChatNotifier sends the sender as the actor, never for Exec2Exec. The server also keeps an actor on /self only from the caller's own organisation. - Spec "As built": the notifications paragraph. Verified: tsc; eslint (nothing on changed lines); the repo's 19 guards; vitest (notifications, members, lib, settings, chat; the few that timed out under machine load pass on their own); and a Chrome probe that the name stays inline in the toast's two-line clamped title and the history row's ellipsis still holds.
· kamo-shared-library
The Notes app is "Notes & Checklists"
The Notes app holds checklists as well since 2026-09-23 (steps ticked off as they are done, a step that waits until the steps before it are finished, a checklist scheduled on the calendar), and it is called "Notes & Checklists" everywhere it is named. ServiceType.NOTES carries the new display name and a description that covers both, which is what Apps & Features, Branch Types and the org-create wizard show once SecurityService is rebuilt. Copy only: the constant name and id are unchanged, so no stored grant or feature row moves. ServiceTypeCustomerCopyTest pins the name and that the description mentions checklists.
· MediaService
The Hold'em invite and the meeting reminder say who they are from
MemberNotificationClient.raise gains an overload carrying actorMemberId/actorName (the id sent as text; the old signature is unchanged and sends neither). HoldemInviteMessenger passes the dealer ("Dana Reyes dealt you in"); MeetingReminderSweep passes the meeting's creator when the title names them ("Weekly sync · Dana Reyes"), reading the creator once rather than inside title(). EmailService keeps the pair and kamo-internal draws that name as the member's card. An EmailService older than the fields ignores them. Tests: HoldemInviteMessengerTest (+1; its raise verifications take the new overload), MeetingReminderActorTest (2), **************** (2). Full suite: 1060 run, 0 failures.

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