Rooms in the Product

How a permanent room attaches to a real talent and a real brand, 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.

Three 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 three 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
talent-homeOne talentThe talent's first visit to their agentPermanent — exactly one per talent account, forever
dealOne talent, one brand (possibly still anonymous)A visitor starts chatting through that talent's linkPermanent — one per conversation the visitor started; a pair can hold several

Only playground rooms are meant to be thrown away. The other two are permanent by design: a talent's relationship with their agent does not end when one negotiation does, and a brand returning to the same talent should find their conversation again, not a blank one. A talent-brand pair is not forced down to a single room, though — each conversation a visitor actually started is its own room, kept apart on purpose.

The talent's one ongoing room

A talent has exactly one talent-home room, created the first time they open their agent and resolved by their account id on every visit after — there is no "which room was I in" question, because there is only ever one. 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 autoPlan: false and goals.talent-setup. Memory distillation runs on each turn via distilMemory: true so facts are recorded and the setup goal is evaluated. When the visitor signs in, claimOnboardingRoom binds the seat to their account, and runOnboardingPromotion runs the full planner plus the Arvist to promote the interview answers into durable account memory, turning autoPlan: true on for the room's normal ongoing life.

Binding that room's talent participant to a real account is what connects the two layers: the account's durable memory (whatever the Arvist has promoted about this person so far) is seeded into the room the moment the binding happens, so a returning talent's agent already knows them even though the room itself was just created. 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 talent'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 brand participant carries that guest key instead of an account. They negotiate as themselves; the room simply doesn't know their name yet ("A visitor").

flowchart LR Link["/z/:slug"] --> Who{"Signed in?"} Who -->|no| Guest["guest cookie →<br/>newest still-empty guest room,<br/>or a fresh one"] Who -->|yes| Picker["their conversations with<br/>this talent (picker),<br/>or a fresh room"] Guest --> Room[("deal room")] Picker --> Room

A bare revisit to /z/:slug reuses the visitor's newest guest room with that talent 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 brand's room and /dashboard/agent/update/:roomId a talent's, which is what push emails link to. A signed-in brand with several conversations with the same talent gets a picker of them on the chat page, plus "new conversation" for a fresh one.

Binding a real account into a room — at creation for a signed-in brand, at claim for a guest — is what seeds it from durable memory, which is why a deal room never repeats talent setup: the talent's terms were already settled and promoted back in their home room. The deal room's goal is narrowed to just the negotiation.

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 brand side is an unclaimed guest, a planner push — to the guest or to the talent — 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 talent's inbox never shows a room whose sends are all still parked.
  • The offer run is held. A negotiation that concludes while the brand 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 brand'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 brand's real name, durable memory seeds in, 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: the open route 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 talent's link, it is tagged as a brand account automatically. A person's role is inferred from how they arrived, not asked for.

Sessions divide the conversation; the account never sees the room

Internally, "room" is a plumbing concept — a talent's dashboard shows a plain list of conversations instead: their agent's own thread, plus one per brand conversation that has reached them, most recently active first. Selecting one opens the same chat either way; the account never has to know it is a room, let alone which kind. Deal rooms whose brand side is still an anonymous guest stay hidden — every send into them is held anyway — and surface, with the brand's real name, the moment the guest claims them.

A talent can also archive a conversation they are done with: it moves to an archived list (reachable from the bottom of the sidebar) and can be brought back from there at any time. Archiving is per-pane bookkeeping — the room stays fully alive, the agent keeps working in it, and the brand's side notices nothing. The one conversation that can never be archived is the talent's own thread with their agent.

flowchart TB Home[("Home room<br/>(your agent)")] Deal1[("Deal room — Brand A")] Deal2[("Deal room — Brand B")] Inbox["Talent's conversation list<br/>(most recent first)"] Home --> Inbox Deal1 --> Inbox Deal2 --> Inbox

Inside a single conversation, a further seam is visible: sessions, the same 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 — a small but deliberate choice, since it is the same distinction a human would make ("last time we spoke" versus "right now").

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 talent'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 talent's request for a conversation is checked against their own account id; a brand'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 brand 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 brand 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 talent's persona still delivers it to them as pending their approval — the full lifecycle on the previous page holds exactly as written, just inside a room that will still be there for the next brand, 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 talent hears the news (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 talent, /z/:slug/:roomId for the brand. 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 talent'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.