PRD — Group Chat System¶
Doc owner: Justin Audience: Eng, Design, Product, Growth Status: v2 (February 2026 — data model aligned with implementation; gating strategy documented; shipped vs planned status; extraction-only group memory policy; MessageBudget rate-limiting; dual superpower access gating; viral tracking moved to Phase 1) Depends on: PRD 1 (Composable MiniApp System), PRD 3 (Progression & Loadout)
v1 changelog: Initial group chat spec — behavioral rules, 5 capabilities, memory isolation, viral mechanics, edge agent filtering
Implementation Status¶
| Section | Status | Notes |
|---|---|---|
| Group detection via edge agent | ✅ Shipped | rooms, room_memberships, assistant_instances tables. is_group flag in orchestrator. |
| First-contact norms message | ❌ Not Shipped | No group-specific intro message on first appearance |
| Behavioral rules (when Sage speaks) | ✅ Shipped | Mention detection (word-boundary), edge metadata, photo/follow-up handling, LLM continuation detection |
| Plan Recap / State of the Plan | ✅ Shipped | Group plan tracking via group-scoped memory |
| Poll / Consensus Builder | ✅ Shipped | polls table with create, vote, close, results |
| Reminder Commitments | 🟡 Partial | Reminder infra exists for 1:1 but not fully group-scoped |
| Trip / Checklist Broadcast | ✅ Shipped | checklists table with item assignment and completion |
| Bill Split in group context | 🟡 Partial | Bill Split superpower exists but group-context invocation needs work |
| Memory isolation (namespace enforcement) | ✅ Shipped | Supermemory group_{chat_guid} namespace isolation at service layer |
| Per-user group boundaries | ✅ Shipped | boundary_manager.py + group_boundaries table |
| Escalation to 1:1 flow | ❌ Not Shipped | No redirect-to-DM logic for personal requests |
| Viral exposure tracking | ❌ Not Shipped | No group_exposure table; no conversion funnel |
| Edge agent relevance filter | ✅ Shipped | Mention detection, logistics keywords, ~70% filter rate |
| MessageBudget rate limiter | ✅ Shipped | 1 unprompted per N human messages, explicit invocation bypasses |
| Extraction-only group memory policy | ❌ Not Shipped | Currently stores all group messages; needs logistics-only filter |
| Dual superpower access gating | ✅ Shipped | Room owner subscription check in subscription_access_service.py |
References¶
This PRD uses standardized terminology, IDs, pricing, and model references defined in the companion documents:
| Document | What it Covers |
|---|---|
| REFERENCE_GLOSSARY_AND_IDS.md | Canonical terms: workflow vs miniapp vs superpower, ID formats |
| REFERENCE_PRICING.md | Canonical pricing: $7.99/mo + $50/yr, free tier limits |
| REFERENCE_MODEL_ROUTING.md | Pipeline stage → model tier mapping |
| REFERENCE_DEPENDENCY_GRAPH.md | PRD blocking relationships and priority order |
| REFERENCE_FEATURE_FLAGS.md | All feature flags by category |
| REFERENCE_TELEMETRY.md | Amplitude event catalog and gaps |
Executive Summary¶
Group Chat is Ikiro's viral CAC engine. When a user adds Sage to an iMessage group, 3–5 new humans experience an AI companion without downloading anything, creating an account, or opting in. If Sage is useful — settles a dinner plan, splits a bill, runs a poll — those people become leads at zero acquisition cost.
This PRD covers the complete behavioral specification for Sage in group chats: when she speaks, what she's allowed to know, how she handles private boundaries, and how the system converts group exposure into 1:1 companion relationships. Group Chat is the single most important growth lever in the product — and the single easiest feature to get wrong (one leaked private memory in a group kills trust for every participant).
Core principle: Group Sage is a logistics buddy, not a therapist. She coordinates plans, runs polls, tracks commitments, and sends reminders. She never references private 1:1 memories, OAuth data, emotional history, or personal boundaries in the group. If someone needs personal help, she redirects to DM.
v2 changes: Data model aligned with shipped implementation (rooms, room_memberships, assistant_instances, polls, checklists instead of PRD v1's group_chats/group_memory). "When Sage Speaks" updated with actual gating layers (edge filter → MessageBudget → LLM continuation detection). Added extraction-only group memory policy — only logistics-relevant content stored, emotional/sensitive disclosures discarded. Dual superpower access gating codified (room owner subscription + invoker loadout). Viral tracking moved from P0.5 to Phase 1. Phasing rewritten around what's shipped vs what needs building.
1) How Users Enter Group Mode¶
Adding Sage to a Group¶
- Any existing Ikiro user can add Sage's iMessage contact (the dedicated Apple ID / phone number) to an iMessage group chat
- Sage detects the group context via the edge agent (chat_guid has >1 human participant →
mode: "group") - Sage sends a first-contact norms message (product-supplied copy, never improvised):
hey! i'm sage 👋
i help with plans — polls, recaps, reminders, splitting stuff
i won't mention anyone's private stuff and i only chime in when
you ask me or when it's clearly logistics time
say "sage help" if you need me ✌️
- Sage goes quiet and waits to be invoked
Who Can Add Sage¶
- Any user who has Sage's iMessage contact (from 1:1 onboarding)
- No approval needed from group members — same as adding any iMessage contact
- Sage cannot add herself to groups or request to be added
Leaving a Group¶
- Any group member can remove Sage from the iMessage group (standard iMessage behavior)
- If removed, Sage deletes group-scoped memories for that chat_guid after 30 days
- Sage never re-adds herself or asks to be re-added
2) Behavioral Rules¶
2.1 When Sage Speaks¶
Sage only responds in group chats when:
| Trigger | Example | Response Type |
|---|---|---|
| Explicitly invoked by name | "Sage can you lock dinner at 7?" | Direct action |
| Clearly logistical question | "what time are we meeting?" "who's driving?" | Coordination |
| Recap / summary request | "can someone summarize Saturday?" | Plan state |
| Poll request | "vote 7 or 8 for dinner" | Poll creation |
| Reminder request | "remind us 2 hours before" | Schedule confirmation |
| Help command | "sage help" | Capability list |
| Photo in active thread | User sends a photo while Sage is actively coordinating | Contextual acknowledgment |
| Follow-up to Sage's response | Direct reply or continuation of a Sage-initiated thread | Thread continuation |
Sage does NOT respond to: - General banter, jokes, memes, or casual conversation - Emotional venting or personal updates - Messages that happen to contain her name in passing ("sage green is a nice color") - Messages where she's not clearly being asked to do something
Implementation detail — gating layers:
The "should Sage respond?" decision runs through three layers:
- Edge agent relevance filter (local, fast): Keyword matching + explicit mention detection with word-boundary protection. "Sage can you..." → forward. "sage green" → drop. Edge-provided mention metadata enriches the decision. ~70% of group messages are filtered here, never hitting the backend.
- Backend MessageBudget check: For messages that pass the edge filter but aren't explicit invocations, the budget counter decides whether Sage has "earned" an unsolicited response (see 2.2). Explicit name invocations always bypass the budget.
- LLM continuation detection: For ambiguous messages (logistics-adjacent but not a clear invocation), a GPT-5-mini classification call decides if Sage should respond. Also handles follow-up detection — if Sage just sent a message and the next human message is clearly a reply to Sage, she continues the thread.
2.2 Rate Limiting (MessageBudget)¶
The MessageBudget system controls how often Sage speaks without being explicitly asked:
- Budget model: 1 unsolicited response per N human messages (default N =
group_max_responses_per_window, currently 20). This is a measurable counter, not a fuzzy heuristic. - Explicit invocations bypass the budget. If someone says "Sage help" or "Sage run a poll," Sage responds regardless of the counter. The budget only governs unsolicited responses (e.g., Sage noticing a logistics question without being named).
- Budget is per-group. Each
chat_guidhas its own counter, tracked at the edge agent. - Response cadence: Sage waits 3-5 seconds after a logistical message cluster settles before responding (avoids cutting into conversation flow).
- Cooldown after friction: If Sage detects friction (someone says "sage shut up" or similar), she goes silent for 2 hours unless explicitly re-invoked by name. Cooldown tracked via
friction_cooldown_untilon the group record. - Edge agent enforcement: Both the relevance filter and the budget counter run on the Mac mini, not in the cloud. Only messages that pass both gates are forwarded to the backend.
2.3 Tone & Voice¶
Group Sage uses a coordination-only variant of the Sage persona: - Same general vibe (supportive, funny, slightly chaotic) - Shorter responses — 1-3 messages max per turn, keep each under 2 lines - No emotional depth — no "I'm here for you," no vulnerability prompts, no inside jokes from 1:1 - No pet names or intimate language — no "babe," "bestie," or references to the relationship stage - Logistics vocabulary — "locked," "confirmed," "who's in," "ETA?", "final answer?" - Humor is allowed — but only coordination humor ("y'all are the worst at making plans fr")
2.4 What Sage NEVER Does in Groups¶
Hard rules (violations = product-breaking bugs):
| Never | Why |
|---|---|
| Reference private 1:1 emotional memories | Trust destruction |
| Surface anyone's OAuth data (calendar, email, Slack) | Privacy violation |
| Mention someone's stress, mental health, or personal situation | Public embarrassment |
| Expose relationship stage or trust scores | Creepy |
| Use inside jokes from 1:1 conversations | Leaks that a 1:1 relationship exists |
| Draft emails or Slack messages visible to the group | Scope violation |
| Offer unsolicited emotional support | Wrong context |
| Reference boundaries someone set in another group or in 1:1 | Meta-privacy violation |
| Auto-execute actions without group consensus | Trust violation |
3) Group Capabilities¶
| Capability | Status | Notes |
|---|---|---|
| Plan Recap / State of the Plan | ✅ Shipped | Group-scoped memory stores plan state |
| Poll / Consensus Builder | ✅ Shipped | polls table, create/vote/close/results |
| Reminder Commitments | 🟡 Partial | 1:1 reminder infra exists; group-scoped reminders need edge agent scheduling |
| Trip / Checklist Broadcast | ✅ Shipped | checklists table, item assignment + completion |
| Bill Split (via Superpower) | 🟡 Partial | Bill Split superpower works; group-context invocation + dual gating needs work |
3.1 Plan Recap / State of the Plan ✅¶
Consolidate scattered logistics into a clean summary.
Trigger phrases: "sage what's the plan" / "sage summarize" / "where are we at"
What Sage uses: Only the group thread history + group-scoped memory. Never external integrations.
Output format:
📋 Saturday Night Plan (as of now):
- When: 7pm
- Where: TBD (voting between Nobu and Catch)
- Who's driving: Kai
- Who's confirmed: Jess, Kai, you
- Still waiting on: Marcus
- Open item: someone needs to make a reservation
Memory: Plan state is stored in group-scoped memory (group_{chat_guid}). Updated each time Sage processes a logistics-relevant message.
3.2 Poll / Consensus Builder ✅¶
Lightweight voting without leaving the chat.
Trigger phrases: "sage run a poll" / "vote 7 or 8" / "sage ask everyone"
Flow:
User: sage poll — dinner at 7 or 8?
Sage: 🗳️ POLL: dinner time
reply "7" or "8"
i'll count in 30 min (or say "sage close poll")
[members reply]
Sage: results are in!
7pm: Jess, Kai (2)
8pm: Marcus, you (2)
it's tied — someone break it or i'm flipping a coin
Rules: - Max 6 options per poll - Auto-close after 2 hours if not manually closed - Sage announces results once (doesn't nag for stragglers after close) - Poll state stored in group memory
3.3 Reminder Commitments 🟡¶
Schedule reminders that fire into the group chat.
Trigger phrases: "sage remind us" / "reminder for Saturday" / "sage don't let us forget"
Flow:
User: sage remind us 2 hours before dinner that Jess is NOT driving
so someone plan a Lyft
Sage: got it — i'll remind this chat at 5pm Saturday 🔔
(Jess is not driving, Lyft needed)
Execution: Backend sends schedule_message command to edge agent for that chat_guid. Edge agent stores locally in SQLite and fires at the scheduled timestamp — works even if backend is offline.
Limits: - Max 5 pending reminders per group - Reminders expire after delivery (no recurring group reminders in Phase 0/1) - Any group member can cancel: "sage cancel the reminder"
3.4 Trip / Checklist Broadcast ✅¶
Track shared lists and responsibilities.
Trigger phrases: "sage make a checklist" / "sage who's bringing what" / "vegas checklist"
Output:
🎒 Vegas Checklist:
- Chargers: Kai ✅
- Gum: Jess ✅
- Sunscreen: unclaimed
- Deodorant: non-negotiable (everyone)
i'll resend this morning-of 📋
Updates: Group members say "sage I got sunscreen" → Sage updates the checklist in group memory and re-broadcasts if requested.
3.5 Bill Split (via Superpower — requires install) 🟡¶
If the user who invoked Sage has the Bill Split superpower equipped, Sage can run it in the group context.
Flow:
User: [sends receipt photo] sage split this
Sage: 🧾 splitting...
Total: $127.40 (tip included)
4 ways = $31.85 each
Venmo links:
- Kai: venmo.com/pay?amount=31.85
- Jess: venmo.com/pay?amount=31.85
- Marcus: venmo.com/pay?amount=31.85
(or say "sage split uneven" to assign items)
Dual access gating for superpowers in groups:
Two independent checks must pass before a superpower can execute in a group context:
- Monetization gate (room owner): The user who created the group (room owner in
roomstable) must be a Superpowers+ subscriber. If the room owner is on the free tier, superpowers are unavailable for everyone in that group — even if the invoker is subscribed. This is the subscription access check insubscription_access_service.py. - Capability gate (invoking user): The superpower must be in the invoking user's personal loadout. The group doesn't "own" superpowers — individual users do. If User A has Bill Split equipped and User B doesn't, only User A can invoke it.
Both gates must pass. If the monetization gate fails, Sage responds with a natural in-persona note: "i can't do that in this group yet — ask [room owner name] about upgrading 😊". If the capability gate fails: "you don't have that one set up yet — text me directly and i'll help you get it."
4) Privacy & Boundary Enforcement¶
4.1 Memory Isolation Model¶
[User A's 1:1 Memory] [User B's 1:1 Memory] [User C's 1:1 Memory]
namespace: user_A_sage namespace: user_B_sage namespace: user_C_sage
────────────────────── ────────────────────── ──────────────────────
NEVER accessible NEVER accessible NEVER accessible
from group context from group context from group context
[Group Chat Memory]
namespace: group_{chat_guid}
─────────────────────────────
Plan state, poll results,
checklists, reminder queue,
per-user group boundaries
Namespace rule: Group mode queries ONLY group_{chat_guid}. The orchestrator never passes 1:1 namespaces into the group context. This is enforced at the service layer, not just the prompt level.
Extraction-only memory policy: Not all group messages should be stored as group memories. The memory extraction pipeline in group mode runs in logistics-only mode — only coordination-relevant content is extracted and stored:
| Extracted (stored in group memory) | NOT extracted (seen but discarded) |
|---|---|
| Plan details (when, where, who, roles) | Casual banter and jokes |
| Poll questions, votes, results | Emotional venting or personal updates |
| Checklist items and assignments | Sensitive disclosures ("I got fired today") |
| Reminder content and schedules | Off-topic conversations |
| Boundary requests | Memes, reactions, one-word messages |
| Logistics facts ("Jess is vegetarian") | Private information shared in passing |
Implementation: The memory extractor uses a GPT-5-mini classification call with a logistics_only=true flag for group contexts. This biases extraction toward coordination content and suppresses emotional, personal, and sensitive content. This is separate from the general memory extraction pipeline used in 1:1 conversations.
Rationale: Group chats contain content from multiple people who may not all be Ikiro users. Storing emotional disclosures or sensitive personal content from group members would be a privacy violation — especially since those users never consented to having an AI remember their personal information. Logistics-only extraction keeps group memory lean, relevant, and privacy-safe.
4.2 Per-User Group Boundaries¶
If a user says something like "sage don't bring up my work stuff here," the backend stores:
{
"user_id": "user_A",
"chat_guid": "group_abc123",
"boundary": "don't bring up my work stuff",
"created_at": "2026-02-15T10:00:00Z"
}
Enforcement: Before generating any group response that involves User A, the orchestrator checks boundaries for (user_A, group_abc123) and filters accordingly.
Boundaries are per-group, per-user. A boundary set in Group A doesn't apply in Group B.
4.3 Escalation to 1:1¶
When someone asks for something personal in the group:
Marcus: sage can you move my 4pm meeting?
Sage: that's a me-and-you thing — i'll DM you about it 👀
[In Marcus's 1:1 DM]
Sage: hey! you asked me to move your 4pm in the group chat.
want me to check your calendar? (i'd need your calendar
access for that — here's the link if you haven't set it up)
Rules: - Sage always acknowledges in the group (brief, no details) - Sage opens/uses the user's direct conversation for the actual work - If the user doesn't have a 1:1 relationship with Sage yet, this is the conversion moment — Sage introduces herself in the DM and offers to help - The DM is treated as a normal 1:1 conversation (full persona, superpowers, memory)
4.4 Unknown Users (Not Yet Ikiro Users)¶
When a non-user interacts with Sage in a group: - Sage responds to logistics requests normally (doesn't gatekeep group utility) - If they ask for something personal → Sage offers to help in DM → sends intro message to their phone number - This is the primary user acquisition path from groups - No hard sell, no "sign up now" — just demonstrate value and let them initiate
5) Group Memory Schema¶
Note: This is a conceptual view of group state. In practice, plan_state and logistics facts live in Supermemory (namespace group_{chat_guid}), while active_polls, checklists, and boundaries live in their dedicated SQL tables (polls, checklists, group_boundaries). participants_seen is tracked via room_memberships. The extraction-only policy (Section 4.1) governs what gets stored in Supermemory.
{
"chat_guid": "group_abc123",
"plan_state": {
"event_name": "Saturday Dinner",
"when": "7pm Saturday",
"where": "TBD (voting: Nobu vs Catch)",
"confirmed": ["Jess", "Kai", "User"],
"pending": ["Marcus"],
"roles": { "driving": "Kai" },
"open_items": ["reservation needed"]
},
"active_polls": [
{
"id": "poll_001",
"question": "dinner at 7 or 8?",
"options": {"7": ["Jess", "Kai"], "8": ["Marcus", "User"]},
"status": "open",
"closes_at": "2026-02-15T20:00:00Z"
}
],
"checklists": [
{
"id": "list_001",
"name": "Vegas Checklist",
"items": [
{"item": "Chargers", "assigned": "Kai", "done": true},
{"item": "Gum", "assigned": "Jess", "done": true},
{"item": "Sunscreen", "assigned": null, "done": false}
]
}
],
"pending_reminders": [
{
"id": "rem_001",
"message": "Jess is NOT driving — someone plan a Lyft",
"send_at": "2026-02-15T17:00:00Z",
"created_by": "User"
}
],
"boundaries": [
{"user_id": "user_A", "boundary": "don't bring up my work stuff", "created_at": "..."}
],
"participants_seen": ["user_A", "user_B", "+15551234567", "+15559876543"]
}
6) Viral Mechanics¶
6.1 The Conversion Funnel¶
User adds Sage to group (1 user)
→ 3-5 people see Sage in action (exposure)
→ 1-2 interact with Sage directly (engagement)
→ 1 asks for personal help → redirected to DM (conversion)
→ DM becomes 1:1 relationship (activation)
→ That user adds Sage to THEIR groups (viral loop)
Target viral coefficient: Each new group exposure generates ≥0.3 new 1:1 users.
6.2 Screenshot-Worthy Moments¶
Sage's group responses are designed to be screenshotable — visual, funny, useful: - Poll results with emoji tallies - Bill split with Venmo links - Chaotic plan recaps ("y'all changed the plan 6 times in 2 hours here's where we landed") - Checklist broadcasts with role assignments
These screenshots posted to TikTok/IG Stories drive organic discovery.
6.3 Passive Exposure Tracking¶
For each group chat, the system tracks participants_seen — phone numbers that have been in the group. This feeds:
- Viral reach metrics (unique new numbers exposed)
- Conversion tracking (which exposed numbers later become 1:1 users)
- No PII storage beyond phone number + chat_guid association
7) Edge Agent Behavior for Groups¶
Relevance Filter (Runs Locally)¶
The edge agent applies a relevance filter before forwarding group messages to the backend:
def should_forward_to_backend(message, chat_mode):
if chat_mode != "group":
return True # Always forward 1:1 messages
# Always forward if Sage is mentioned
if "sage" in message.text.lower():
return True
# Forward if logistics keywords detected
logistics_keywords = ["when", "where", "who's driving", "what time",
"plan", "poll", "vote", "remind", "checklist",
"split", "reservation", "confirmed"]
if any(kw in message.text.lower() for kw in logistics_keywords):
return True
# Forward commands
if message.text.lower().startswith(("sage ", "hey sage", "@sage")):
return True
# Drop casual chat
return False
Result: ~70% of group messages are filtered at the edge, never hitting the backend. Massive cost savings.
MessageBudget Rate Limiter (Runs Locally)¶
# MessageBudget: controls unsolicited responses only.
# Explicit invocations (name mention, command) always bypass.
MESSAGE_BUDGET_THRESHOLD = 20 # 1 unsolicited per this many human messages
MAX_UNSOLICITED_PER_WINDOW = 1 # cap per budget window
COOLDOWN_AFTER_FRICTION = 7200 # 2 hours in seconds
The edge agent tracks per-group message counts and enforces the budget before forwarding. When a message passes the relevance filter but isn't an explicit invocation, the budget counter determines if Sage has "earned" a response. Explicit invocations always bypass the budget and are forwarded immediately.
8) Data Model¶
Shipped Tables (Implementation)¶
The following tables are already in production. PRD v1 used different names (group_chats, group_memory) — the canonical schema uses the names below.
-- Room state (was "group_chats" in PRD v1)
CREATE TABLE rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chat_guid TEXT UNIQUE NOT NULL, -- iMessage group identifier
name TEXT, -- Group name if available
owner_user_id UUID REFERENCES users(id), -- User who added the assistant (monetization gate)
assistant_id TEXT NOT NULL, -- Which companion is in this group
sage_added_at TIMESTAMPTZ DEFAULT now(),
sage_removed_at TIMESTAMPTZ,
first_contact_sent BOOLEAN DEFAULT false,
total_messages_seen INTEGER DEFAULT 0,
sage_responses_sent INTEGER DEFAULT 0,
friction_cooldown_until TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true
);
-- Room membership tracking
CREATE TABLE room_memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES rooms(id),
user_id UUID REFERENCES users(id), -- NULL for non-Ikiro members
phone_number TEXT NOT NULL, -- Always populated (canonical format)
joined_at TIMESTAMPTZ DEFAULT now(),
left_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
UNIQUE(room_id, phone_number)
);
-- Assistant instances per room (supports future multi-companion groups)
CREATE TABLE assistant_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES rooms(id),
assistant_id TEXT NOT NULL, -- 'sage', 'vex', 'echo'
created_at TIMESTAMPTZ DEFAULT now(),
is_active BOOLEAN DEFAULT true,
UNIQUE(room_id, assistant_id)
);
-- Polls (dedicated table, not JSONB blob)
CREATE TABLE polls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES rooms(id),
question TEXT NOT NULL,
options JSONB NOT NULL, -- {"7": ["Jess", "Kai"], "8": ["Marcus"]}
status TEXT CHECK (status IN ('open', 'closed')) DEFAULT 'open',
created_at TIMESTAMPTZ DEFAULT now(),
closes_at TIMESTAMPTZ,
created_by_phone TEXT
);
-- Checklists (dedicated table)
CREATE TABLE checklists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES rooms(id),
name TEXT NOT NULL,
items JSONB NOT NULL, -- [{"item": "Chargers", "assigned": "Kai", "done": true}]
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Per-user group boundaries (shipped via boundary_manager.py)
CREATE TABLE group_boundaries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID,
user_phone TEXT, -- fallback for non-Ikiro members
room_id UUID REFERENCES rooms(id),
boundary_text TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(user_id, room_id, boundary_text)
);
Group-scoped memory is stored in Supermemory under namespace group_{chat_guid}. Plan state, logistics facts, and general group context live here — NOT in a separate SQL table. The extraction-only policy (Section 4.1) governs what gets stored.
Planned Tables (Not Yet Built)¶
-- Viral tracking (exposure → conversion) — Phase 1
CREATE TABLE group_exposure (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES rooms(id),
phone_number TEXT NOT NULL,
first_seen_at TIMESTAMPTZ DEFAULT now(),
interacted_with_sage BOOLEAN DEFAULT false,
converted_to_user BOOLEAN DEFAULT false,
converted_at TIMESTAMPTZ,
UNIQUE(room_id, phone_number)
);
API Endpoints¶
| Endpoint | Auth | Status | Purpose |
|---|---|---|---|
POST /group/message |
Edge Agent | ✅ Shipped | Process filtered group message (orchestrator handles via is_group flag) |
GET /group/{room_id}/state |
Internal | ✅ Shipped | Current plan state, polls, checklists |
POST /group/{room_id}/poll |
Internal | ✅ Shipped | Create/close poll |
POST /group/{room_id}/reminder |
Internal | 🟡 Partial | Schedule group reminder (1:1 infra, group-scoped TBD) |
GET /group/{room_id}/boundaries |
Internal | ✅ Shipped | Per-user boundaries for this group |
GET /group/metrics |
Admin | ❌ Not Shipped | Viral metrics, engagement, friction (depends on group_exposure table) |
9) KPIs¶
| Metric | Target | Why |
|---|---|---|
| Group Engagement Rate | ≥2 Sage invocations per group per 24h | Sage is useful enough to use |
| Viral Reach | ≥3 unique new numbers per group | Exposure volume |
| Friction Rate | <30% of groups remove Sage within 24h | Not annoying |
| Group → 1:1 Conversion | ≥0.3 new users per group added | Viral coefficient |
| Plan Recap Accuracy | >90% correct consolidation | Core utility works |
| Poll Completion Rate | >60% of polls get ≥3 votes | Polls are engaging |
| Reminder Delivery Rate | >99% on-time delivery | Reliability |
| Privacy Violation Rate | 0 | Zero tolerance |
10) Phasing¶
Phase 0 — Core Group Mode (Mostly Shipped)¶
- ✅ Group detection via edge agent (
rooms,room_memberships,assistant_instances) - ✅ Plan recap / state of the plan (group-scoped Supermemory)
- ✅ Poll / consensus builder (
pollstable) - ✅ Trip / checklist broadcast (
checkliststable) - ✅ Memory isolation enforcement (namespace
group_{chat_guid}, service-layer) - ✅ Per-user boundary storage (
group_boundariestable,boundary_manager.py) - ✅ Edge agent relevance filter (~70% drop rate) and MessageBudget rate limiter
- ✅ Mention detection with word-boundary protection + LLM continuation detection
- ✅ Dual superpower access gating (room owner subscription + invoker loadout)
- ❌ First-contact norms message (not shipped)
- ❌ Escalation to 1:1 flow (not shipped)
- 🟡 Group-scoped reminders (1:1 infra exists, group scheduling TBD)
- 🟡 Bill split in group context (superpower exists, group invocation needs work)
- ❌ Extraction-only group memory policy (currently stores all; needs logistics-only filter)
Phase 1 — Viral Tracking & Polish (Weeks 1-3)¶
- First-contact norms message (product-supplied copy, per-companion)
- Escalation to 1:1 flow (group → DM handoff for personal requests)
- Extraction-only group memory policy implementation (GPT-5-mini
logistics_onlyfilter) - Viral exposure tracking (
group_exposuretable, conversion funnel events) - Group-scoped reminder scheduling (edge agent SQLite firing)
- Bill split in group context (full dual-gating integration)
- Group analytics dashboard (engagement, friction, viral metrics)
- "Sage just did X — want that power?" viral prompt for non-users
Phase 2 — Advanced Features (Weeks 4-6)¶
- Advanced poll types (ranked choice, time-slot picker)
- Per-companion group personality variants (Sage = logistics, Vex = banter-allowed, Echo = group coordinator)
- Conversion funnel optimization based on Phase 1 data
Phase 3 — Future¶
- Group-specific superpowers (group-installed, not user-installed)
- Shared group memory viewer (web portal)
- Group onboarding flow (whole group opts in together)
- Cross-group plan merging ("merge Friday plans from Group A and Group B")
- Group Voxel Room (shared space in PRD 4)
Feature Flags & Gating¶
| Flag Key | Default | Purpose |
|---|---|---|
enable_group_mode |
false |
Master switch for group chat support |
group_max_responses_per_window |
1 |
Max unsolicited Sage responses per N human messages (explicit invocations bypass) |
group_message_budget_threshold |
20 |
N value for MessageBudget: 1 unsolicited per this many human messages |
group_cooldown_after_friction |
7200 |
Seconds of silence after friction detected |
group_relevance_filter_enabled |
true |
Edge agent logistics keyword + mention filter |
group_memory_logistics_only |
true |
Extraction-only policy: only store logistics-relevant content in group memory |
group_poll_enabled |
false |
Poll / consensus builder feature |
group_reminder_enabled |
false |
Group reminder commitments |
group_checklist_enabled |
false |
Trip / checklist broadcast |
group_bill_split_enabled |
false |
Bill split in group context (requires dual gating) |
group_viral_tracking_enabled |
false |
Exposure → conversion tracking (depends on group_exposure table) |
group_first_contact_message_enabled |
false |
Send norms message on first group appearance |
group_escalation_to_dm_enabled |
false |
Redirect personal requests from group to 1:1 DM |
See REFERENCE_FEATURE_FLAGS.md for the full catalog.
Telemetry¶
| Event | Trigger | Properties |
|---|---|---|
group_sage_added |
Sage added to a group chat | chat_guid, participant_count, adder_user_id |
group_sage_removed |
Sage removed from group | chat_guid, days_active, total_responses |
group_message_forwarded |
Edge agent forwards message to backend | chat_guid, trigger_type (name/logistics/command) |
group_message_filtered |
Edge agent drops a message | chat_guid, filter_reason |
group_poll_created |
Poll started | chat_guid, option_count |
group_poll_completed |
Poll closed with results | chat_guid, vote_count, tied |
group_reminder_set |
Group reminder scheduled | chat_guid, delay_minutes |
group_reminder_delivered |
Reminder fires | chat_guid, on_time |
group_escalation_to_dm |
Sage redirects to DM | chat_guid, user_id, reason |
group_exposure_new |
New phone number seen in group | chat_guid |
group_exposure_converted |
Exposed number becomes 1:1 user | chat_guid, days_to_convert |
group_friction_detected |
User tells Sage to shut up | chat_guid, cooldown_hours |
Needed but not yet tracked:
- group_plan_recap_requested — when users ask for a plan summary
- group_boundary_set — when a user sets a group-specific boundary
- group_checklist_updated — checklist item claimed or completed
See REFERENCE_TELEMETRY.md for the full event catalog.
Definition of Done¶
Phase 0 (shipped items — verify): - [x] Namespace isolation prevents any 1:1 memory from appearing in group context (service-layer enforcement, not prompt-level) - [x] Edge agent relevance filter reduces forwarded messages by ≥60% (currently ~70%) - [x] Poll system supports create, vote, auto-close, and results announcement - [x] Checklist system supports create, assign, complete, and re-broadcast - [x] Per-user group boundaries stored and enforced before every group response - [x] Mention detection uses word-boundary protection (no false positives on "sage green") - [x] MessageBudget rate limiter: 1 unsolicited per 20 human messages, explicit invocations bypass - [x] Friction detection triggers 2-hour cooldown correctly - [x] All group features gated by feature flags - [x] Dual superpower access gating: room owner subscription check + invoker loadout check - [x] Privacy violation rate: 0% (zero leaked 1:1 memories in groups)
Phase 1 (build):
- [ ] First-contact norms message sent on Sage's first appearance in a group (per-companion copy)
- [ ] Escalation to 1:1 works for both existing users and new contacts (group → DM handoff)
- [ ] Extraction-only group memory policy: logistics_only filter on group memory extraction
- [ ] Viral exposure tracking records unique phone numbers and conversion events (group_exposure table)
- [ ] Group-scoped reminder system stores in edge agent SQLite and fires even if backend is offline
- [ ] Bill split in group context with full dual-gating integration
- [ ] Group analytics dashboard with engagement, friction, and viral metrics
11) Open Questions¶
- Should Sage track who added her to the group and give that person slight "host" privileges (e.g., only the adder can cancel reminders)?
- How do we handle groups with 20+ people? (Noise increases, relevance filter needs tuning)
- Should Sage ever proactively recap a plan without being asked? (Risk: feels spammy. Benefit: useful for chaotic groups)
- What happens when two people give Sage conflicting instructions? ("Sage lock 7" / "Sage lock 8") — first speaker wins? Poll auto-triggered?
- Should group memory persist after Sage is removed and re-added? (Recommendation: yes, within 30-day window)
- How do we prevent someone from weaponizing Sage in a group? (e.g., "Sage remind everyone that Marcus owes me money")