Skip to content

Rooms, grounded meeting agents, and a preset startup team - #343

Open
Deodat-Lawson wants to merge 2 commits into
mainfrom
worktree-dazzling-marinating-spring
Open

Rooms, grounded meeting agents, and a preset startup team#343
Deodat-Lawson wants to merge 2 commits into
mainfrom
worktree-dazzling-marinating-spring

Conversation

@Deodat-Lawson

@Deodat-Lawson Deodat-Lawson commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Two related changes to the collaboration subsystem, plus the measurements that decided the shape of both.

Rooms

A meeting is a conversation: one speaker at a time, elected from the transcript, working toward a close. A room is a query — one question, every member, concurrently, no floor to hold.

POST /api/collab/rooms/{id}/ask   { "text": "What breaks if the token becomes a JWT?" }

  @legal    Clause 11.2 caps liability at 12 months of fees.   ← reads contracts/
  @ops      Nothing in my sources covers that.                 ← reads runbooks/
  @finance  The Growth tier is 58% of self-serve revenue.      ← reads finance/

Members are bound to different document sets, which is the whole point (see the measurements below). askRoom is a function rather than a method on MeetingOrchestrator for three concrete reasons: stepInner marks the whole meeting failed when no runtime serves a persona, which is the ordinary case for an unreachable room member; a room carries no state between rounds, so there is nothing to persist; and maxConsecutiveFailures is meaningful for a conversation and wrong for a fan-out.

  • Members are isolated — each sees the question only, never the other answers. Otherwise the first to finish anchors the rest and it stops being a fan-out.
  • Each settles independently — answered, declined, failed, timed out, or unserved. Failures are system messages so the minutes extractor and the Slack bridge never read an error as content.
  • Rounds have no table. The question carries its id and expected roster, answers carry the round id, summarizeRounds derives the rest from the log — the same rule the rest of this subsystem follows.
  • Retrieval runs as the asking human, never the room's creator. The meeting path grounds using the creator's grants; copying that here would make a room a way to read documents you could not open yourself. A member whose sources return nothing declines without consulting the model at all.

No turn policy, moderator, minutes, completion marker, or control route — a room has no state machine to drive.

Grounded meeting agents

Meeting agents were the one part of the product that reasoned about the workspace's documents without ever reading them: MeetingConfig.context existed and nothing populated it. Retrieval is now a port on the engine (TurnGroundingProvider), consulted before each turn for the persona about to speak. Per-persona, because the analyst and the engineer ask the corpus different questions; capped at four passages, because each turn already carries the whole transcript; never fatal, because a meeting that dies when the index blinks is worse than an ungrounded one.

Turns record what they read — label, page, score, truncated excerpt — which keeps a grounded transcript checkable without the index still being around. Behaviour change: evaluateMeeting's grounding dimension now reads those excerpts as well as config.context. Before this, a meeting grounded purely by retrieval scored "no context supplied — dimension not applicable" at weight zero.

Also adds a Core 6 preset team (founder, product, eng, design, growth, data), applied additively — a handle already in use is reported, never overwritten, because persona keys are referenced by past transcripts and frozen meeting rosters.

Speaker election fix

findMention took the first @mention in a message. A real turn opens by answering the last speaker and closes by asking someone else, so the floor kept returning to whoever had just spoken and the specialists were never reached — in one live run @eng and @growth never spoke at all despite being addressed repeatedly. The last mention now wins.

Every scripted line in the suite carries exactly one mention, which is why no existing test moved. This was found by running a real meeting.

How the shape was chosen

Three live harnesses ship with this (meeting:live, meeting:compare, meeting:probe). What they found, over 39 runs against real models:

  • Six personas sharing one corpus scored no better than a single agent, at 3–5× the wall-clock. Not evidence against rooms — evidence against rooms of clones. A room whose members read the same material has nothing for deliberation to discover.
  • A planted-false-premise probe found no separation either (7/9 vs 8/9): both sides caught explicitly-stated corrections every time, and on the one probe needing arithmetic both failed the same way — reading the correcting passage, citing it, then continuing to plan on the false premise.
  • Two apparent wins dissolved on inspection. "The room hallucinates less" was the single agent doing correct arithmetic my metric counted as untraceable; "the room assigns owners" was partly an artifact of the extractor only resolving owners to roster handles.

That is why rooms bind members to different document sets rather than shipping more personas over a shared one.

Follow-up

External sessions — a Claude Code or Codex session joining from another machine — are #342, along with the hub security floor that gates them. One item there is live today and independent of this PR: listKnownNodes binds any connected node to whichever company's settings page loads first.

Verification

  • 1,671 tests pass, 0 fail (19 suites skipped — the Postgres-gated integration ones), including the three-process two-machine distributed test.
  • Typecheck clean across apps/web and packages/adapters; lint clean; check-core-facade ok.
  • Migrations generated with drizzle-kit (never hand-written — snapshot lineage has broken before); drizzle-kit check reports no drift.
  • evals:meetings still passes with good/bad separation intact.

Not done: the UI for rooms. The API is the deliverable here; a rooms pane modelled on MeetingsPane is a small follow-on.

🤖 Generated with Claude Code


Note

High Risk
Touches collab orchestration, document retrieval authorization (asker-scoped room RAG), and speaker routing—security-sensitive access patterns plus behavior changes in live meetings.

Overview
Adds collab rooms (one question, all members in parallel, each from its own documents), per-turn meeting grounding via a TurnGroundingProvider port, a startup-core preset persona pack with additive apply APIs, and fixes @mention speaker election to use the last handoff mention.

Rooms introduce askRoom, channel-log–derived rounds (summarizeRounds), isolated member context (TurnContext.mode: "room"), and web APIs plus pdr_ai_v2_collab_room storage. Members settle independently (answered / declined / failed / timeout / unserved); retrieval runs as the asker, not the room creator.

Meeting grounding wires retrieval before each turn (per speaking persona, non-fatal on errors), stores citation excerpts on messages, and extends evaluateMeeting grounding scoring to use those excerpts—not only pinned config.context. Meetings gain grounding_enabled and document_ids columns.

Presets ship GET/POST /api/collab/agents/presets with idempotent, non-overwriting handle application.

Speaker election: under reactive and moderated, findMention now prefers the last valid @handle (skipping self-mentions), fixing floors returning to the previous speaker on multi-mention turns.

Reviewed by Cursor Bugbot for commit 30af2c5. Bugbot is set up for automated code reviews on this repo. Configure here.

Deodat-Lawson and others added 2 commits August 16, 2026 21:18
…up team

Meeting agents were the one part of the product that reasoned about the
workspace's documents without ever reading them: `MeetingConfig.context`
existed and nothing populated it. Retrieval is now a port on the engine —
`TurnGroundingProvider` — consulted before each turn for the persona about to
speak, implemented over the existing ensemble retriever. Per-persona, because
the analyst and the engineer ask the corpus different questions; capped at four
passages, because each turn already carries the whole transcript; never fatal,
because a meeting that dies when the index blinks is worse than an ungrounded
one.

Turns record what they read — label, page, score and a truncated excerpt — which
keeps a grounded transcript checkable without the index still being around and
lets `evaluateMeeting` tell a cited number from an invented one. Before this, a
meeting grounded purely by retrieval scored "no context supplied — dimension not
applicable" at weight zero.

Adds a Core 6 preset team (founder, product, eng, design, growth, data) applied
additively: a handle already in use is reported, never overwritten, because
persona keys are referenced by past transcripts and frozen meeting rosters.

Also fixes speaker election. `findMention` took the *first* @mention in a
message, but a working turn opens by answering the last speaker and closes by
asking someone else — so the floor kept going back to whoever had just spoken
and the specialists were never reached. The last mention now wins. Every
scripted line in the suite carries exactly one mention, which is why no existing
test moved; this was found by running a real meeting.

Adds three live harnesses that found all of the above: `meeting:live`,
`meeting:compare` (one agent vs the room on identical evidence) and
`meeting:probe` (planted false premises).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A meeting is a conversation — one speaker at a time, elected from the
transcript, working toward a close. A room is a query: one question, every
member, concurrently, no floor to hold. They share a channel log and nothing
else.

This is the shape the measurements pointed at. Six personas reading one shared
corpus scored no better than a single agent across 39 live runs, at 3-5x the
wall clock, because a room of members who all read the same material has nothing
for deliberation to discover. A room is worth exactly as much as the information
its members do not share — so members here are bound to different document sets,
and retrieval runs per member.

`askRoom` is a function rather than a method on `MeetingOrchestrator`.
`stepInner` marks the whole meeting failed when no runtime serves a persona,
which is the ordinary case for a room member that is unreachable; a room carries
no state between rounds, so there is nothing to persist; and
`maxConsecutiveFailures` is meaningful for a conversation and wrong for a
fan-out. Each member settles independently as answered, declined, failed, timed
out, or unserved, and a failure is a `system` message so the minutes extractor
and the Slack bridge never read an error as content.

Members are isolated: each sees the question only, never the other answers.
Otherwise the first to finish anchors the rest and it stops being a fan-out.

Rounds have no table. The question carries its id and expected roster, answers
carry the round id, and `summarizeRounds` derives the rest from the log — the
same rule the rest of this subsystem follows.

Retrieval runs as the asking human, never the room's creator. The meeting path
grounds using the creator's grants; copying that here would make a room a way to
read documents you could not open yourself. The room's document set narrows the
corpus, the asker authorizes it. A member whose sources return nothing declines
without consulting the model at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
launch-stack Error Error Aug 17, 2026 1:30am
pdr-ai-v2 Error Error Aug 17, 2026 1:30am

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_793082e3-20c1-4c76-9a9d-2c85a2fff164)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30af2c502b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +230 to +232
for (const persona of toCreate) {
await createPersona(companyId, persona);
created.push(persona.key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make preset application atomic under concurrent requests

When two tabs or replicas apply the same preset concurrently, both can read the same pre-insert roster and enter this loop with identical toCreate lists. The unique (company_id, key) index then rejects one request—potentially after it has already inserted earlier personas—so the advertised idempotent operation returns a 500 and leaves a partially applied pack. Use conflict-safe inserts or a transaction that derives created/skipped from the actual writes.

Useful? React with 👍 / 👎.

Comment on lines +278 to 279
...(grounding.sources ? { grounding: grounding.sources } : {}),
...result.meta,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve host retrieval provenance over runtime metadata

When a custom or remote runtime returns a meta.grounding field, this trailing spread overwrites the sources produced by the host's grounding provider. The transcript UI and scoreGrounding() subsequently treat the runtime-supplied value as the passages actually retrieved, corrupting citations and evaluation results; the same spread order exists in settleMember() for rooms. Merge runtime metadata first and assign host-controlled grounding afterward.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant