Rooms in the Product

How a permanent room attaches to a real host and a real visitor, and what each side reaches through the product

The previous pages describe the mediator on its own terms: rooms, participants, memory. This page is the seam — how those rooms are found from the product's real surfaces (an account, a dashboard, a public link), and what changes when a participant is a real person with a real account rather than a name typed into a playground.

Accounts have no stored user type. Bouncer and Scout are default capabilities; Roster is inferred or explicitly enabled. Deal rooms use host / visitor seats. See Capabilities, Seats, and Derived Role.

Four kinds of room, one mechanism

Every room still follows the same rules — participants never speak to each other, the planner mediates — but a room is now one of four kinds, and the kind is how the product finds the right one instead of creating a new one every time.

KindParticipantsCreated whenLifetime
playgroundWhoever an operator addsAn operator starts one, for testing behaviourDisposable
homeOne hostThe account's first agent setupPermanent — exactly one per account
dealOne host, one visitor (possibly still anonymous)A visitor starts chatting through that host's link, or the talent brings an off-platform thread via Bring a conversation to ZPermanent — one per conversation; a pair can hold several
demoOne host (your account), one unbound simulated visitorTest agent starts a runOne room per rehearsal; listed as past tests. See Test agent

Only playground and demo rooms are meant to be thrown away. home and deal are permanent by design: a host's relationship with their agent does not end when one negotiation does, and a visitor returning to the same host should find their conversation again, not a blank one. A host-visitor pair is not forced down to a single room, though — each conversation a visitor actually started is its own room, kept apart on purpose. After a deal is booked, or after a written offer has sat quiet long enough (Z_AGENT_NEW_OFFER_QUIET_DAYS, default 7), a new commercial ask does not rewrite that row. Code asks whether to open a sibling deal room; chat yes never edits a booked offer (ZLY-1933 / ZLY-1934). A demo rehearsal is a throwaway inbound against the real host agent; it never mints a live offer.

The account-level home room carries service = agent. Bouncer and Scout share that home conversation and its durable account memory.

The host's one ongoing room

A host has exactly one account-level home room, created during setup and resolved by account id on every visit after. It is where setup happens the first time, and it is also just their agent's inbox going forward: the same room hears about a deal that closed, answers a question about their rates, and gets updated when their situation changes, indefinitely.

During initial onboarding (/onboarding), this room is created for the visitor as a guest with goals.account-setup. Interview turns skip the background memory pass so they stay cheap. When the visitor signs in, the seat is bound to their account, Bouncer and Scout are enabled, and one promotion pass runs Distil plus the Arvist to promote the interview answers into durable account memory. After that, every Personalize turn Distils.

Binding that room's host participant to a real account is what connects the two layers: the account's single durable memory (whatever the Arvist has promoted so far) is seeded into the room the moment the binding happens, so a returning host's agent already knows them even though the room itself was just created. After the agent is live, each Update Agent Distil runs Arvist for the host so a new preference is durable before they test z-link (ZLY-1897). If the Arvist writes again later — a deal room went quiet, standing knowledge changed — AccUpdate re-merges that durable document into this room before the next planner run, so a permanent home room does not keep a stale seed. See The Agents and Agent Setup Onboarding.

The host's public link — the same /z/:slug a wizard used to sit behind — now opens the chat for anyone, signed in or not. An anonymous visitor gets a guest identity in an HttpOnly cookie the moment they arrive, and a fresh deal room whose visitor participant carries that guest key instead of an account. They negotiate as themselves; the room simply doesn't know their name yet ("A visitor").

A bare revisit to /z/:slug reuses the visitor's newest guest room with that host as long as it is still empty — refreshing the page never accumulates blank rooms — and otherwise starts a fresh one, so a conversation they already had keeps its history distinct from a new approach. Guests are capped: at most 8 open guest rooms per cookie, and 11 messages per room before signing in.

Every conversation is also addressable: /z/:slug/:roomId deep-links a visitor's room and /dashboard/agent/update/:roomId a host's, which is what push emails link to. A signed-in visitor with several conversations with the same host gets a picker of them on the chat page, plus "new conversation" for a fresh one.

A talent can also create a deal room from an off-platform thread — Bring a conversation to Z. That URL is still /z/:slug/:roomId, but the visitor seat starts unbound (no guest cookie). The brand hits a welcome first; claiming that link must not mint a second room via ensureDealRoomForGuest. A bare /z/:slug stays the inbound path above and must not resume that invite room.

Binding a real account into a room — at creation for a signed-in visitor, at claim for a guest — is what seeds it from durable memory, or from the home room's host branch when that document is newer (the window between Distil and Arvist). That is why a deal room never repeats host setup: the host's terms were already settled in their home room. The deal room's goal is narrowed to just the negotiation (goals.negotiation). Visitor account memory is not seeded; see Identity and Representation. If the same brand already has a deal room with this host, claim folds the guest sitting into that room (offer rooms first, otherwise the oldest pair room) so login does not leave a duplicate inbox row. A second thread is the signed-in New conversation action — or a sibling room for a new offer when a booked deal cannot be rewritten, or when they return after 7 days of silence and choose a new offer. After that quiet period, a bare /z/:slug visit keeps the real thread instead of starting a blank sitting.

Anonymous until the agent needs to act: the login gate

Chatting works anonymously up to a point. The gate is not at the page — it is in the mechanism, and it fires exactly when the agent is ready to move the deal and can't, because moving it requires a real account on both ends:

  • Every outbound send is held. While the visitor side is an unclaimed guest, a planner push — to the guest or to the host — is not delivered. The persona's message is still composed at that moment (the room is frozen by the gate, so it cannot go stale) and parked on a durable pending effect; the host's inbox never shows a room whose sends are all still parked.
  • The offer run is held. A negotiation that concludes while the visitor is anonymous records its goal flip, but minting the formal offer needs a real buyer account, so the new-offer action is parked the same way.
  • Held effects raise the gate. The visitor's view reports loginRequired with a reason — held ("I was just about to share an update…") or cap (the free-message allowance ran out) — and the composer is replaced by a sign-in card. The turn that triggered the hold still finishes; the next one is refused.

Signing in claims the room: the guest participant is bound to the account, the room is renamed from "A visitor" to the visitor's real name, and every held effect replays in the order it was parked — the pre-composed pushes deliver verbatim (no second model call), and the held offer is minted. Returning to the room signed-in is the claim: openVisitorRoom claims on the way in when the caller's cookie matches, so the first authenticated visit never 404s. Once claimed, the guest cookie no longer opens the room — only the account that claimed it does.

A related, small policy rides along: the first time an account signs in through a host's Z Link, they are acting as a visitor. That does not write an account type — role is inferred from the seat they occupy in this room, not stored on the account. Their own account already has Bouncer and Scout available.

Sessions divide the conversation; the account never sees the room

Internally, "room" is a plumbing concept — the unified dashboard shows a list of deal conversations instead, most recently active first, while the permanent home thread is accessible via Update agent (/dashboard/agent/update). Selecting a deal opens the live conversation thread with toolbar controls. Deal rooms whose visitor side is still an anonymous guest stay hidden — every send into them is held anyway — and surface, with the visitor's real name, the moment the guest claims them.

The inbox displays both inbound and outbound communications with clear directional indicators:

  • "They came to you" (cyan reverse arrow) — inbound visitor outreach.
  • "Z reached out" (gray arrow) — outbound communications.

A host can also archive a conversation they are done with using the folder filter (Active / Archived) and restore it at any time. Archiving is per-pane bookkeeping — the room stays fully alive, the agent keeps working in it, and the visitor's side notices nothing.

Inside a single conversation, several views are available:

  • Conversation Thread: Full transcript with interleaved or side-by-side two-column split view (desktop toggle ?layout=split showing external and internal turns).
  • Offer View: One-page offer document (?view=offer). The tab stays disabled until Offer-Gen mints a row. Approve is the only physical write. Counter, Reject, and Add switch to Conversation as chat turns to that side's agent; Z asks last-look, and only a chat-confirmed counter is then written to the offer, which shows as Countered. Brand-first Approve is thanks only; after talent Approve the brand sees View payment → Send Payment. Details: Offer approval and brand payment.
  • Agreement View: Lawyer-reviewable draft that AgreementAg writes from the live Offer (not from the chat). Opening it generates or refreshes the draft if it is missing or stale. The reader must acknowledge the disclaimer before they can download or print (?view=agreement).
  • Sessions: The boundary the planner uses to avoid re-reading the whole history (see Memory). Reopening a conversation after days away shows a divider marking where the new sitting starts.

Unread is a stored marker, not a guess about the messages

The list marks conversations with something new in them — a count, the name in bold. That cannot be derived from the transcript: the rooms are permanent and the agent speaks unprompted, so "the last message is from the agent" says nothing about whether anyone has read it. Each participant therefore carries a last_read_at, and unread is what the agent said in that pane after it.

Reading a conversation and fetching its pane are treated as the same act: the route that opens a host's pane marks it read. The chat already re-reads the stored transcript when a turn finishes and when the tab regains focus (that is how an unprompted push appears in an open pane), so the marker stays current without a separate "mark read" call the client could forget to send. The chat turn route deliberately does not mark anything — a turn is followed by an answer the reader has not seen yet.

Two consequences worth naming. The list polls, because a push can land in a room the reader is not in, and nothing else would notice. And the open conversation never shows a badge whatever the last count said, which keeps a refresh that raced the read-marking write from decorating the row the reader is looking at.

Ownership is derived from the session, never trusted from the client

Every route that opens a conversation resolves whose it is from the authenticated session — never from a room id or participant id the client happens to send. A host's request for a conversation is checked against their own account id; a visitor's request for a deal room is checked against theirs. For an anonymous visitor, the guest cookie plays the account's role: it opens a room only while that room's visitor participant is still unclaimed and carries the same guest key. A mismatch is reported as not-found, identically to a room that never existed — there is no separate "forbidden" response that would confirm someone else's conversation exists at all.

This is why a deep-linked room id in a shared email is safe to have in a URL: the id alone opens nothing. The internal playground and ops APIs — which do operate on bare room ids, by design — are admin-gated for the same reason; they are operator tools, not a second door into someone's conversation.

This is the same principle the planner's private notes and the guidance routing already rely on (see Memory): a boundary that matters is enforced by the mechanism checking an id against the session, not by a client behaving honestly.

What a participant's browser is allowed to receive

Every reply is stored with the agent-facing context that produced it: the memory slice recall chose, any account-memory merge, and the planner's instruction behind a wake. That record is what tells a bad prompt from missing context when a reply looks wrong, and the playground renders it.

It never reaches the person the agent is talking to. The recall snapshot is streamed only to an admin-gated operator audience, and a pane served to its own participant has those parts stripped — otherwise a visitor chatting through a z-link could read the guidance the agent is negotiating under straight out of the response body, which is the leak the branch stripping on the way in exists to prevent in the first place. Whether the snapshot is stored does not depend on who was watching; only whether it is sent does. A wake keeps its marker so the pane can still badge the message as agent-initiated, but not the instruction text behind it.

From a settled negotiation to a real offer

Nothing about how a negotiation concludes changes because the room it happened in is permanent. The planner still rules the negotiation concluded, Offer-Gen still drafts the formal offer from the agreed terms, and the host's persona still delivers it to them as terms agreed in chat — the written offer is next, and a booking only when that document reaches a BOOKED stage. The full lifecycle on the previous page holds exactly as written, just inside a room that will still be there for the next visitor, or the next negotiation with this one. And when the conversation keeps moving after the offer was drafted, Offer-Update brings the document back in line before the host hears the news — and a material rewrite of an APPROVED offer clears a one-sided brand stamp so both must Approve again. AgreementAg then projects the current Offer into the legal draft shown in the agreement view — it never re-reads the room (see The Agents).

A delivered push also leaves the app: every wake that lands sends the recipient an email with the message and a deep link straight back into that conversation — /dashboard/agent/update/:roomId for the host, /z/:slug/:roomId for the visitor. In the app, the conversation list marks the push unread without the recipient having to open the pane and find it. An unbound participant (a guest) gets no email — there is no address to send to, which is one more reason their pushes are held until they claim.

The wizard, replaced but not deleted

The host's public link used to lead to a step-by-step campaign wizard. That flow still exists in code, reachable at a separate route, but the link itself now leads to the chat described on this page. Nothing about the wizard was removed — it was disconnected from the primary path, in case the chat needs a fallback while it proves itself.