Files
docs/event flow.md
-Puter e6685203fe feat: initial docs repo with project inventory and all documentation
- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info
- Added .gitignore
- Synced all existing docs from local workspace
- Centralized documentation hub for GrowQR team
2026-06-22 15:04:27 +05:30

849 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

> **Missions are not chat flows. Missions are event-driven product journeys.**
> Chat/Grow Agent is only one optional surface that can read mission state and help the user decide what to do next.
The mission dashboard should work even if the user never opens chat.
I read the Rivet docs you linked, the current backend mission/actor code, the old `growqr-app/orchestrator`, and the service event patterns. The correct production design is a **mission event system + dashboard projection**, not a chat-first workflow.
---
# Revised product model
## User buys a mission
Example:
> **Interview-to-Offer Accelerator — $10/month**
Includes limits like:
- 10 mock interviews/month
- X resume analyses/optimizations
- maybe 1 final readiness report
- optional add-ons later: roleplay, extra interviews, video analysis, etc.
After purchase, the user gets an **active mission dashboard**.
The dashboard shows:
- current mission progress
- next required action
- completed service actions
- generated artifacts
- interview/resume results
- Q Score/readiness trend
- quota usage
- recommendations
Chat can help, but the dashboard is the primary surface.
---
# Important correction for Interview-to-Offer
If the team says this mission currently uses only:
- Resume
- Interview
Then dont force Roleplay into the core package.
Roleplay can become:
1. an upsell,
2. a recommended recovery action,
3. a premium variant,
4. or a later stage only if interview results show communication weakness.
So the core mission becomes:
## Interview-to-Offer Accelerator v1
### Included services
- Resume Building
- Mock Interview
- Q Score/readiness analytics
- Grow Agent/planning only as backend intelligence, not necessarily chat
### Core stages
1. Purchase / mission activated
2. Resume uploaded or selected
3. Resume analyzed/improved for target interview
4. Interview prep plan generated
5. Mock interview configured
6. Mock interview completed
7. Review received
8. Final readiness score/report generated
That is much cleaner.
---
# What exists today
From the code:
## Backend already has
- `grow_active_missions`
- mission actors like `interviewToOfferMissionActor`
- mission snapshots
- explicit APIs to update stages and artifacts
- PG-first persistence for active missions
- Q Score snapshots/workflow event tables from older workflow infra
But right now, mission progress updates mostly happen through explicit backend calls like:
```txt
POST /missions/active/:instanceId/stages/:stageId
POST /missions/active/:instanceId/artifacts
```
That means the dashboard does not automatically know when a microservice finishes something unless we explicitly update the mission.
## Old orchestrator already had a useful pattern
The old `growqr-app/orchestrator`:
- routes actions to services
- receives `agent_data` messages
- extracts Q Score signals from service results
- publishes signals to `qscore.signal_events`
- handles real-time score updates
But it has too much routing registry logic:
```py
ACTION_TO_AGENT
PAGE_TO_AGENT
COMPOSITE_WORKFLOWS
```
We should not rebuild that as another large registry.
## Services emit useful events/messages already
Examples:
Resume emits A2A-style messages:
- `resume_created`
- `resume_loaded`
- `version_saved`
- `ai_analysis_complete`
- `ai_summary_optimized`
- `ai_skills_suggested`
- `export_ready`
Interview emits:
- `interview_configured`
- `session.completed` over live WS
- review endpoint returns `completed`, `processing`, `failed`, `not_eligible`
- `review_loaded` through agentic path
Roleplay emits similar patterns.
Q Score already has a strong event ingestion path:
```txt
qscore.signal_events Redis stream
```
So the missing layer is not “service capability.” It is a **GrowQR mission event ingestion/projection layer**.
---
# Correct architecture
## Principle
> Postgres is the source of truth. Actors are live reducers/projectors. Chat is just one consumer.
## Event flow
```txt
Service action happens
GrowQR backend receives/creates event
Raw event is stored in Postgres event inbox
Event is forwarded to the relevant mission actor
Mission actor updates mission snapshot/progress/artifacts
Dashboard reads updated mission snapshot
Q Score signals are published/updated
Grow/chat may optionally read the same state
```
---
# New concept: dynamic mission events
We do **not** need a rigid event registry.
We need one flexible event envelope.
## Generic event envelope
```ts
type GrowEvent = {
id: string;
userId: string;
orgId?: string;
source: string;
// resume-service, interview-service, roleplay-service, qscore-service, grow-backend
type: string;
// resume.ai_analysis_complete
// interview.session_completed
// interview.review_completed
// mission.purchased
// qscore.updated
category:
| "mission"
| "service"
| "artifact"
| "usage"
| "qscore"
| "entitlement"
| "system";
occurredAt: string;
mission?: {
instanceId?: string;
missionId?: string;
stageId?: string;
};
subject?: {
kind: string;
id: string;
};
// example: { kind: "interview_session", id: sessionId }
correlation?: {
requestId?: string;
sessionId?: string;
resumeId?: string;
externalId?: string;
};
payload: Record<string, unknown>;
};
```
The important thing: **we store unknown events too.**
If an event type is new, we dont reject it. We save it, attach it to the user/mission if possible, and show it in debugging/timeline if useful.
---
# Postgres tables needed
Do not rely on actor state for durable history.
Add something like:
## `grow_events`
Append-only event inbox.
Fields:
- `id`
- `user_id`
- `mission_instance_id`
- `source`
- `type`
- `category`
- `subject_kind`
- `subject_id`
- `correlation_id`
- `payload`
- `occurred_at`
- `received_at`
- `processed_at`
- `processing_status`
- `error`
## `mission_service_sessions`
Tracks external service objects created for a mission.
Examples:
- interview session ID
- resume ID
- roleplay session ID
- qscore run ID
Fields:
- `id`
- `user_id`
- `mission_instance_id`
- `stage_id`
- `service_id`
- `external_type`
- `external_id`
- `status`
- `last_event_id`
- `last_checked_at`
- `created_at`
- `updated_at`
This table is crucial because the dashboard needs to know:
> “This missions interview stage corresponds to interview session `abc123`.”
## `mission_artifacts`
Could either reuse current snapshot artifacts or make durable table.
Examples:
- resume fit scan
- interview prep plan
- interview review
- readiness report
---
# Actor design
Do **not** put this all in `growActor`.
`growActor` is becoming chat/conversation/user home state. Missions should not depend on it.
## Best actor model
### 1. Mission actor per active mission
Already exists:
```txt
interviewToOfferMissionActor[userId, instanceId]
```
Extend it conceptually to support:
```ts
ingestEvent(event)
recomputeProjection()
scheduleExternalCheck()
getDashboardSnapshot()
```
The actor stores only bounded state:
- current stage
- progress
- latest known service session IDs
- latest scores
- artifact summaries
- next action
- last few events maybe
Raw events stay in Postgres.
### 2. Optional user analytics actor
A per-user analytics actor can exist later:
```txt
userAnalyticsActor[userId]
```
Its job:
- update user-level Q Score trend
- aggregate events across missions
- maintain dashboard analytics
- detect recommendations
But dont make it mandatory for v1. The mission actor + PG event store is enough.
### 3. Do not create one god event actor
Avoid:
```txt
globalEventProcessorActor
```
That becomes a bottleneck.
Better:
```txt
event ingest route → Postgres → mission actor for that mission/user
```
Rivet docs support actor queues and schedules, which are good primitives here. Use queues for ordered event processing and schedules for later status checks. The docs also note actor `noSleep` is deprecated in current docs, so we should avoid relying on permanent awake actors long-term, even though the project is pinned to `rivetkit ^2.2.1`.
---
# Event ingestion paths
We need multiple ingestion paths because not every service currently emits backend webhooks.
## 1. Backend gateway-generated events
Whenever the dashboard calls GrowQR backend gateway:
```txt
POST /services/interview/configure
POST /services/resume/resumes/:id/analyze
POST /services/interview/review/:sessionId
```
The backend should emit internal events like:
```txt
interview.configured
resume.analysis_requested
resume.analysis_completed
interview.review_completed
```
This is the easiest first layer.
## 2. Service webhook/event endpoint
Add a backend route:
```txt
POST /events/ingest
```
Services can call this when they complete things:
```txt
resume.ai_analysis_complete
interview.session_completed
interview.review_completed
roleplay.review_completed
```
This is ideal long term.
## 3. Redis stream bridge
Old services already use Redis streams for task responses.
We can add a backend consumer that listens to service response messages and converts them into Grow events.
But I would not make Redis the only source of truth. It should feed Postgres.
## 4. Polling/checker fallback
This is mandatory.
Some events will be missed. Some services currently only expose state through REST. Some live WebSocket events go straight to the browser.
So when a mission creates an interview session, store:
```txt
mission instance → interview session id
```
Then schedule checks:
```txt
check interview session/review in 30s
check again in 2m
check again in 5m
stop when completed/failed/expired
```
Actors are good for this because Rivet supports scheduled actions.
---
# How the mission updates automatically
Example: Interview-to-Offer.
## User buys mission
Backend creates event:
```txt
mission.purchased
```
Mission actor receives it:
- status: active
- stage: resume
- progress: 010%
- next action: upload/select resume
Dashboard shows:
> Next: Update your resume for this interview.
---
## User analyzes resume
Dashboard calls Resume service through backend.
Backend stores:
```txt
resume.analysis_requested
```
When result returns or poller finds result:
```txt
resume.ai_analysis_complete
```
Mission actor applies dynamic reducer:
- source = resume-service
- event contains resume analysis
- mission stage = resume
- mark resume stage done
- create artifact: Resume Fit Scan
- update progress
- next action: Start mock interview
No chat required.
---
## User starts interview
Backend gateway calls Interview service configure.
Backend stores:
```txt
interview.configured
```
Mission actor:
- links session ID to mission
- marks interview stage ready/in_progress
- next action: Start interview session
---
## User completes interview
Live WS sends `session.completed` to browser today.
But backend should know through one of:
1. service webhook,
2. backend polling review endpoint,
3. frontend callback to backend,
4. Redis bridge if session completion is emitted there.
Backend stores:
```txt
interview.session_completed
```
Mission actor:
- stage status: review pending
- next action: wait for review / view processing
---
## Review becomes available
Backend fetches:
```txt
GET /services/interview/review/:sessionId
```
If completed, store:
```txt
interview.review_completed
```
Mission actor:
- creates artifact: Mock Interview Review
- marks interview stage done
- extracts score/weaknesses
- publishes Q Score signals
- computes/requests final readiness
- updates dashboard
---
# Q Score integration
Q Score should be event-fed, not chat-fed.
Current old orchestrator extracts signals from service messages. We can reuse that idea but move it into backend event processing.
## Signal mapper should be dynamic-ish
Instead of a giant event registry, use a few generic extractors by category/source:
```txt
resume event → resume signal extractor
interview event → interview signal extractor
roleplay event → roleplay signal extractor
engagement/mission event → workflow signal extractor
```
This is not a mission registry. It is a small set of signal extractors.
Example interview review signals:
```txt
interview.completed
interview.overall_score
interview.language_score
interview.voice_score
interview.body_language_score
interview.filler_words
interview.technical_quality
```
Resume signals:
```txt
resume.uploaded
resume.ats_compatibility
resume.keyword_relevance
resume.grammar_clarity
resume.impact_statements
```
Mission signals:
```txt
mission.interview_to_offer.started
mission.interview_to_offer.resume_completed
mission.interview_to_offer.interview_completed
mission.interview_to_offer.completed
```
These are then sent to Q Score.
---
# Dashboard behavior
The mission dashboard should be driven by mission state, not chat messages.
## Active mission page
For Interview-to-Offer:
```txt
Interview-to-Offer Accelerator
Progress: 55%
Plan: $10/month
Usage:
- Interviews: 2 / 10
- Resume analyses: 1 / 3
Current step:
Mock Interview
Next action:
Start your 15-minute behavioral mock interview.
Completed:
✓ Mission purchased
✓ Resume selected
✓ Resume analyzed
✓ Resume fit scan generated
Artifacts:
- Resume Fit Scan
- Interview Prep Plan
- Mock Interview Review, pending
Readiness:
Baseline Q Score: 64
Current Q Score: 71
Delta: +7
```
## Stage cards
Each card has its own CTA:
Resume stage:
- Upload resume
- Select resume
- Analyze resume
- View fit scan
Interview stage:
- Configure mock interview
- Start interview
- View review
Final stage:
- Generate readiness report
- View final Q Score
No chat required.
---
# Chat/Grow Agents new role
Chat becomes a reader/writer of the same mission system.
It can:
- explain current mission status
- recommend next action
- call mission/service tools
- summarize artifacts
- answer “what should I do next?”
But if the user never opens chat, the mission still progresses.
So the Grow Agent should not own the mission. It should consume the mission projection.
---
# Subscription/usage tracking
For paid missions, usage should also be event-based.
Events:
```txt
entitlement.granted
usage.interview_session_created
usage.interview_session_completed
usage.resume_analysis_requested
usage.resume_analysis_completed
usage.limit_reached
subscription.renewed
subscription.cancelled
```
The gateway should enforce limits before creating expensive service sessions.
Example:
```txt
User clicks Start Interview
Backend checks entitlement
If allowed, create interview session
Insert usage event
Increment monthly usage
```
Do not rely on frontend hiding buttons.
---
# What I would build first
## Phase 1 — Mission event backbone
No new product logic yet. Just make events durable.
- `grow_events`
- `mission_service_sessions`
- `/events/ingest`
- backend helper: `recordGrowEvent()`
- event forwarding to mission actor
- mission actor `ingestEvent()`
## Phase 2 — Backend gateway emits mission events
Whenever dashboard uses service routes, backend records events.
Examples:
- interview configured
- interview review fetched
- resume analyzed
- resume version saved
## Phase 3 — Mission actor becomes projection reducer
Add generic dynamic handling:
- store unknown events
- update timeline
- if event matches current stage service, update stage
- attach artifacts if payload has review/analysis/export
- update next action
## Phase 4 — Polling fallback
For linked external sessions:
- poll interview review until completed
- poll roleplay review until completed
- poll resume analysis state if needed
## Phase 5 — Q Score bridge
Convert service events into Q Score signals and publish them.
## Phase 6 — Dashboard reads projection
Dashboard should not guess. It reads:
```txt
GET /missions/active/:instanceId
```
and renders:
- stages
- events
- artifacts
- next action
- usage
- score trend
---
# Key recommendation
Do not create another large service/action registry.
Use this instead:
1. **Feature registry** remains the product source of truth.
2. **Mission definitions** define stages and included services.
3. **Events are dynamic** and stored raw.
4. **Small generic reducers** interpret categories/sources.
5. **Unknown events are accepted, stored, and visible.**
6. **Actors project events into live mission state.**
7. **Postgres remains authoritative.**
That gives you the cohesive, event-driven mission system youre describing without recreating the old orchestrator registry.