Sibling rooms for a new offer

A booked offer stays frozen; a new deal for the same talent and brand gets its own conversation

Tickets: ZLY-1933 (booked change), ZLY-1940 (rejected offer), and ZLY-1934 (quiet period). One-sided rewrite before booking is ZLY-1932 — out of scope here.

The same talent and brand can have several deal rooms. A booked (or paid) or rejected written offer is frozen in its conversation. A later campaign, or a change they still want after booking or reject, is a new conversation — not a rewrite or reopen of the old Offer tab.

This is not Bring a conversation to Z (an outbound invite from an off-platform thread) and not a bare /z/:slug inbound that resumes the oldest pair room.

What people see

SituationWhat Z doesIf they say yesIf they say no
Brand asks to change a booked, paid, or rejected offerRefuses. Asks whether they want a new offer. Does not rewrite or reopen this Offer tab.New conversation + working Z-Link. Inbox opens it.Stop. The old offer stays.
Talent asks to change a booked, paid, or rejected offerRefuses. Asks whether they want Z to send a message to the brand. Does not create a new conversation.Code messages the brand: this offer is frozen or rejected; they can start a new offer.Stop. Do not contact the brand.
Brand asks for a new offer / new conversation (any stage, including before booking)Creates the sibling room. Does not freeze or refuse this offer.——
Talent asks for a new offerSame as the talent booked-change path. Only the brand can start a new offer.Message the brand.Stop.
First brand message after 7 days of silence (any previous outcome)Asks only: new offer or continuation of the previous one.Same sibling room + Z-Link.Stay. Resume this room.
First talent message after 7 days of silenceDoes not ask new vs continuation. Continues, or offers to message the brand if they want a new deal.——

The original Offer tab is unchanged. The new room starts a fresh negotiation — it does not copy the old offer, campaign, or transcript.

Titles look like Volt Hydration × Maya Ortiz · 17 Sep so the inbox can tell rooms apart.

The brand gets a Z-Link (/z/{slug}/{roomId}). Talent dashboard links are not used to mint this fork. Chat posts that URL as a real markdown link; the UI also navigates when the assistant message carries data-open-room.

Both seats must be signed in. If the brand is still a guest, Z tells them to sign in and try again — it does not invent a fork.

Flow

Why a new room

The booked document will be signed later. A silent rewrite (or a new version on the same row) would make that signature meaningless and, after pay-in, collide with money already held. A second conversation keeps the booked deal intact and lets a new deal start clean.

The same split already existed for inbound sittings: a pair is not forced down to one room. See A deal room per conversation started. This page is the product fork: booked or rejected change, or a new campaign after silence.

Quiet period

7 days with no human message. Agent-only wakes do not reset the clock. One env: Z_AGENT_NEW_OFFER_QUIET_DAYS (default 7). Do not scatter the number in prompts.

Applies regardless of the previous offer’s outcome (open, booked, paid, declined, expired).

A message before 7 days does not ask new vs continuation. If that old offer is already booked and the brand then asks to change it, the booked-refuse path still applies. The talent is never asked new vs continuation — only the brand can start a new offer.

A bare /z/:slug visit normally ensureDealRooms (oldest pair room) and may start a fresh sitting, which hides history and keeps the old Offer tab. After 7 days of silence that looks like a new deal in the old room.

If the pair room is quiet and there is still only one room: skip the fresh sitting so the first message can ask new vs continuation.

After the brand has already minted a sibling new-offer room, a bare /z/:slug visit may land on a blank sitting. That is the current new-offer conversation — not a defect, and not a requirement to reopen the old thread. Deep links with an explicit roomId stay on that room.

How code steers (and does not trust) the model

State branch (persona-owned, like pendingPrincipalDecisions):

  • newDealFork.pending: { kind: "booked-change" | "quiet-period", askedAt }
  • Cleared when they answer. Chat-yes is never a rewrite of the old offer.

Guard JSON includes roomAction (Distil's is ignored in live rooms):

none | ask-quiet-fork | refuse-booked-change | create-sibling-room | stay-in-room | relay-visitor

applyRunEffects allows that action only when the room state matches:

Proposed actionCode allows it only when
create-sibling-roomVisitor seat, and newDealFork.pending is set and this turn is a yes, or the visitor explicitly asked for a new offer/conversation (any stage)
relay-visitorTalent seat, pending booked-change, and they said yes to messaging the brand
stay-in-roompending kind is quiet-period (or they declined a booked new-offer / relay ask)
ask-quiet-forkVisitor seat, room is quiet and pending is empty and they did not already ask for a new offer
refuse-booked-changelive stage is BOOKED_* (or paid / held / completed / disputed) or DECLINED_BY_TALENT

If the room is quiet, code asks the brand in the chat turn. If the stage is booked and a persona or Guard tries to push an amendment of this offer, Guard / applyRunEffects drops that push. Offer-Update still only touches DRAFT / APPROVED. Guard never mints a sibling room.

Last-look / close-flow is gated: not on a booked or rejected offer, not while a quiet-period question is open.

Creating the room

createSiblingDealRoomFrom resolves host + visitor account ids and calls insertDealRoom (always insert). It does not use ensureDealRoom (that resumes the oldest pair). The brand + New conversation HTTP path already used this insert; the sibling is the same primitive.

Tests and inspect

Adaptive prompts (split so a booked rewrite is not used as the first quiet-period message). Only the brand can start a new offer.

  • tests/manual/zly-1933-booked-offer-refuse-new-room.md — freeze this conversation’s booked offer; talent is offered a message to the brand; the brand mints a sibling room.
  • tests/manual/zly-1940-rejected-offer-frozen-new-room.md — freeze a rejected offer the same way; talent is offered a message to the brand; the brand mints a sibling room. The rejected Offer tab stays Rejected.
  • tests/manual/zly-1934-quiet-new-offer-or-continuation.md — after silence on an unbooked offer (no talent Approve), a brand greeting asks new vs continuation; new offer gets its own room.

To exercise the 7-day ask without waiting a week, settle from the room page:

{ "guard": false, "arvist": false, "simulateNewDealQuiet": true }

POST /api/z-agent/my/rooms/:roomId/settle — session cookie, caller’s own room. This is the same settle hook tests already use for quiet-chain work. The extra flag stamps newDealFork.simulateQuiet and ages every sitting in the room past the 30-minute session gap. Real 7-day silence already opens a new sitting on the next human line; the flag alone used to leave the current sitting open, so the agent kept negotiating that thread.

The room memory helper returns isQuiet, quietDays, lastParticipantMessageAt, and newDealForkPending (session + Bearer CRON_SECRET).

Compared with other surfaces

SurfaceWhenRoom
Sibling new offerBrand wants a new offer (after a freeze relay, or after quiet)Always-insert deal for the same pair. Fresh negotiation.
+ New conversationSigned-in brand / talent inboxSame insert. Manual.
Bare /z/:slugBrand opens the public linkResume oldest pair (ensureDealRoom). Fresh sitting unless the room is quiet.
Bring a conversation to ZTalent pastes an off-platform threadOutbound deal, unique invite URL.

Source

PieceWhere
Quiet check, fork answers, roomActionpackages/z-agent/srv/src/sibling-deal-room.ts
Insert + createSiblingDealRoomFrompackages/z-agent/srv/src/rooms.ts
Distil / Guard parsepackages/z-agent/srv/src/agents/planner.ts, guard.ts
Prompt rulesNEW_DEAL_FORK_PROMPT_RULES in default-prompts.ts; DEAL STAGE block in offer-stage.ts / persona-prompt.ts
Open-room part + navigatepackages/z-agent/client/src/lib/open-room.ts, Conversations + Z-Link chat
Dashboard pathDASHBOARD.agent.conversationRoom

Deal stages live in Deals move through explicit physical stages. How /z/:slug normally opens is in Rooms in the Product. Dual Approve and pay-in are in Offer approval and brand payment.