puter fix: service registry and curator onboaridng data fix, home feed route fixes

This commit is contained in:
-Puter
2026-06-24 15:30:00 +05:30
parent 1ecd964104
commit 750a6ab03b
11 changed files with 107 additions and 159 deletions

View File

@@ -15,10 +15,10 @@ const requiredServices = [
"courses-service",
"assessment-service",
"matchmaking-service",
"pathways-service",
"resume-service",
"cover-letter-service",
"qscore-service",
"social-branding-service",
];
const registry = await import("../dist/services/service-registry.js");
@@ -95,7 +95,6 @@ for (const id of requiredServices) {
}
assert(registry.getService("jobs-service")?.id === "matchmaking-service", "matchmaking alias failed");
assert(registry.getService("career-pathways-service")?.id === "pathways-service", "pathways alias failed");
assert(registry.getService("coverletter-service")?.id === "cover-letter-service", "cover-letter alias failed");
assert(registry.getService("q-score-service")?.id === "qscore-service", "qscore alias failed");
assert(registry.getService("social-service")?.id === "social-branding-service", "social alias failed");

View File

@@ -27,7 +27,6 @@ const requiredServices = [
"courses-service",
"assessment-service",
"matchmaking-service",
"pathways-service",
"qscore-service",
"social-branding-service",
];

View File

@@ -52,8 +52,8 @@ Every notification must point to one of these real dashboard routes:
- /agents/roleplay for recruiter/manager/salary/stakeholder roleplay
- /agents/qscore for Q Score/readiness explanations
- /missions for mission progress, approvals, artifacts, next stages
- /social for LinkedIn/social branding
- /pathways for locked/coming-soon pathways
- /agents/social-branding for LinkedIn/social branding
- /agents/matchmaking for Scout/opportunity matching
- /rewards for locked/coming-soon rewards
- /suggestions for broad onboarding/profile suggestions
Every notification object must include:
@@ -65,11 +65,10 @@ Use urgency truthfully: now = needs immediate user action, today = useful today,
function sanitizeHref(href: string, moduleId: HomeModuleId) {
if (isAllowedNotificationHref(href)) return href;
if (href.startsWith("/missions")) return "/missions/active";
if (href.startsWith("/social")) return "/social";
if (href.startsWith("/pathways")) return "/pathways";
if (href.startsWith("/agents/social-branding")) return "/agents/social-branding";
if (href.startsWith("/agents/matchmaking")) return "/agents/matchmaking";
if (href.startsWith("/rewards")) return "/rewards";
if (href.startsWith("/productivity")) return "/productivity";
return moduleId === "productivity" ? "/productivity" : `/${moduleId}`;
return moduleId === "productivity" ? "/agents" : `/${moduleId}`;
}
function stableId(prefix: string, index: number) {
@@ -82,8 +81,8 @@ function sourceFromHref(href: string) {
if (href.startsWith("/agents/roleplay")) return "roleplay";
if (href.startsWith("/agents/qscore")) return "qscore";
if (href.startsWith("/missions")) return "mission";
if (href.startsWith("/social")) return "social";
if (href.startsWith("/pathways")) return "pathways";
if (href.startsWith("/agents/social-branding")) return "social";
if (href.startsWith("/agents/matchmaking")) return "pathways";
if (href.startsWith("/rewards")) return "rewards";
return "system";
}
@@ -115,8 +114,8 @@ function defaultHrefForSource(source: NonNullable<AgentHomeNotification["source"
if (source === "roleplay") return "/agents/roleplay";
if (source === "qscore") return "/agents/qscore";
if (source === "mission") return "/missions";
if (source === "social") return "/social";
if (source === "pathways") return "/pathways";
if (source === "social") return "/agents/social-branding";
if (source === "pathways") return "/agents/matchmaking";
if (source === "rewards") return "/rewards";
return moduleId === "productivity" ? "/agents" : `/${moduleId}`;
}

View File

@@ -37,16 +37,16 @@ const FRESH_MS = 10 * 60 * 1000;
const EXPIRY_MS = 24 * 60 * 60 * 1000;
const SERVICE_HREFS = {
resume: buildServiceLink("resume-service", "workspace") ?? "/opportunities/resume",
interview: buildServiceLink("interview-service", "discovery") ?? "/upskilling/interview",
roleplay: buildServiceLink("roleplay-service", "discovery") ?? "/upskilling/roleplay",
qscore: buildServiceLink("qscore-service", "dashboard") ?? "/home",
resume: buildServiceLink("resume-service", "workspace") ?? "/agents/resume",
interview: buildServiceLink("interview-service", "discovery") ?? "/agents/interview",
roleplay: buildServiceLink("roleplay-service", "discovery") ?? "/agents/roleplay",
qscore: buildServiceLink("qscore-service", "dashboard") ?? "/agents/qscore",
mission: "/missions/active",
social: buildServiceLink("social-branding-service", "profile") ?? "/opportunities/social-media",
pathways: buildServiceLink("pathways-service", "dashboard") ?? "/career-pathways",
social: buildServiceLink("social-branding-service", "profile") ?? "/agents/social-branding",
pathways: buildServiceLink("matchmaking-service", "jobs") ?? "/agents/matchmaking",
rewards: "/rewards",
suggestions: "/suggestions",
productivity: "/productivity",
productivity: buildServiceLink("courses-service", "catalog") ?? "/agents/courses",
} as const;
type SeedNotification = Omit<HomeNotification, "id" | "createdAt"> & { moduleId: HomeModuleId; priority: number };

View File

@@ -25,12 +25,12 @@ const demoNotifications: DemoNotification[] = [
{ moduleId: "missions", title: "2 approvals pending", subtitle: "Resume v6 and Mock #4 feedback need your confirmation.", tag: "Action", urgency: "now", href: "/missions", source: "mission", priority: 113 },
{ moduleId: "missions", title: "Final readiness unlock", subtitle: "Complete one roleplay recovery drill to generate the final checklist.", tag: "Next", urgency: "soon", href: "/missions", source: "mission", priority: 106 },
{ moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/social", source: "social", priority: 104 },
{ moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/social", source: "social", priority: 100 },
{ moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/social", source: "social", priority: 96 },
{ moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/agents/social-branding", source: "social", priority: 104 },
{ moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 100 },
{ moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 96 },
{ moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/pathways", source: "pathways", priority: 70 },
{ moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/pathways", source: "pathways", priority: 68 },
{ moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 70 },
{ moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 68 },
{ moduleId: "productivity", title: "Resume ATS 86", subtitle: "Keyword relevance +9 after JD tailoring. Open Resume Builder.", tag: "Resume", urgency: "today", href: "/agents/resume", source: "resume", priority: 118 },
{ moduleId: "productivity", title: "Interview review is ready", subtitle: "Overall 82 · storytelling and concise examples need one more drill.", tag: "Mock", urgency: "now", href: "/agents/interview", source: "interview", priority: 117 },

View File

@@ -42,8 +42,8 @@ export type HomeFeedResponse = {
export const MODULE_META: Record<HomeModuleId, Omit<HomeModule, "count" | "notifications">> = {
suggestions: { id: "suggestions", label: "Today's Queue", href: "/suggestions", accent: "orange" },
missions: { id: "missions", label: "Missions", href: "/missions", accent: "orange" },
social: { id: "social", label: "Social Branding", href: "/social", accent: "blue" },
pathways: { id: "pathways", label: "Pathways", href: "/pathways", accent: "teal" },
social: { id: "social", label: "Social Branding", href: "/agents/social-branding", accent: "blue" },
pathways: { id: "pathways", label: "Pathways", href: "/agents/matchmaking", accent: "teal" },
productivity: { id: "productivity", label: "Interview · Roleplay · Resume", href: "/agents", accent: "orange" },
rewards: { id: "rewards", label: "Rewards", href: "/rewards", accent: "amber" },
};
@@ -62,9 +62,9 @@ export const ALLOWED_NOTIFICATION_HREFS = new Set([
"/missions",
"/missions/active",
"/missions/available",
"/social",
"/pathways",
"/productivity",
"/agents/social-branding",
"/agents/matchmaking",
"/agents",
"/rewards",
"/agents/resume",
"/agents/interview",

View File

@@ -10,7 +10,6 @@ const LOG_CONTAINERS = {
interview: "interview-service-api-1",
roleplay: "roleplay-service-api-1",
social: "growqr_social_api",
pathways: "pathways-service-api-1",
courses: "courses_service-api-1",
assessment: "assessment-service-api-1",
matchmaking: "matchmaking-service-api-1",

View File

@@ -10,7 +10,6 @@ export type ServiceId =
| "courses-service"
| "assessment-service"
| "matchmaking-service"
| "pathways-service"
| "resume-service"
| "cover-letter-service"
| "qscore-service"
@@ -155,30 +154,27 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
discovery: {
path: "/upskilling/interview",
aliases: ["/agents/interview"],
path: "/agents/interview",
queryParams: ["fresh"],
usage: "Entry screen for role selection and resume of in-progress interview work.",
},
setup: {
path: "/upskilling/interview/setup",
aliases: ["/agents/interview/setup"],
queryParams: ["role", "type", "from_assignment"],
path: "/agents/interview/setup",
queryParams: ["role", "type", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Collects interview role, type, duration, persona, media mode, and personalization consent.",
},
preview: {
path: "/upskilling/interview/preview",
aliases: ["/agents/interview/preview"],
path: "/agents/interview/preview",
queryParams: ["role", "type", "persona", "duration", "difficulty", "media", "vip", "from_assignment", "personalize", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Curator default handoff. The page configures the session and opens the launch overlay.",
},
feedback: {
path: "/upskilling/interview/feedback",
queryParams: ["sessionId"],
path: "/agents/interview/feedback",
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Opens feedback/review for a completed or processing session.",
},
session: {
path: "/v2/service-sessions/interview",
path: "/agents/interview/preview",
queryParams: ["session_id", "goal", "role", "type"],
usage: "Legacy service-session launcher used by service agent results.",
},
@@ -244,30 +240,27 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
discovery: {
path: "/upskilling/roleplay",
aliases: ["/agents/roleplay"],
path: "/agents/roleplay",
queryParams: ["fresh"],
usage: "Entry screen for scenario discovery and resume of in-progress roleplay work.",
},
setup: {
path: "/upskilling/roleplay/setup",
aliases: ["/agents/roleplay/setup"],
queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment"],
path: "/agents/roleplay/setup",
queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Collects roleplay scenario details and stores configure parameters for the builder.",
},
builder: {
path: "/upskilling/roleplay/builder",
aliases: ["/agents/roleplay/builder"],
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief"],
path: "/agents/roleplay/builder",
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief", "vip", "from_assignment_id"],
usage: "Curator default handoff for generating or resuming a roleplay plan.",
},
feedback: {
path: "/upskilling/roleplay/feedback",
queryParams: ["sessionId"],
path: "/agents/roleplay/feedback",
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Opens feedback/review for a completed or processing session.",
},
session: {
path: "/v2/service-sessions/roleplay",
path: "/agents/roleplay/builder",
queryParams: ["session_id", "goal", "role", "type"],
usage: "Legacy service-session launcher used by service agent results.",
},
@@ -330,23 +323,22 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
workspace: {
path: "/opportunities/resume",
aliases: ["/agents/resume"],
path: "/agents/resume",
queryParams: ["tab", "section", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Resume hub. Use tab=resumes by default and section to deep-link editor panels.",
},
editor: {
path: "/opportunities/resume/:resumeId",
path: "/agents/resume/:resumeId",
queryParams: ["section"],
usage: "Resume editor for a known resume.",
},
templates: {
path: "/opportunities/resume/templates",
path: "/agents/resume/templates",
queryParams: [],
usage: "Template gallery.",
},
session: {
path: "/v2/service-sessions/resume",
path: "/agents/resume",
queryParams: ["goal", "role"],
usage: "Legacy service-session launcher used by service agent results.",
},
@@ -404,17 +396,17 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
workspace: {
path: "/opportunities/resume",
path: "/agents/resume",
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Open with tab=cover-letters to land in the cover-letter list.",
},
generator: {
path: "/opportunities/cover-letter",
queryParams: [],
usage: "Standalone generation entry point.",
path: "/agents/resume",
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Cover-letter generation currently lands in the shared resume workspace with tab=cover-letters.",
},
editor: {
path: "/opportunities/resume/cover-letters/:coverLetterId",
path: "/agents/resume/cover-letters/:coverLetterId",
queryParams: [],
usage: "Cover-letter editor for a known coverLetterId.",
},
@@ -444,7 +436,7 @@ const serviceRegistry: ServiceRecord[] = [
{
id: "courses-service",
label: "Courses",
description: "Create, list, and open upskilling courses.",
description: "Create, list, and open learning courses.",
category: "learning",
enabled: Boolean(config.coursesServiceUrl),
featureId: "courses",
@@ -466,9 +458,19 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
catalog: {
path: "/upskilling/course",
path: "/agents/courses",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Course catalog and upskilling entry point.",
usage: "Course catalog and learning entry point.",
},
assigned: {
path: "/agents/courses/assigned",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Assigned course rail.",
},
lesson: {
path: "/agents/courses/assigned/:assignmentCode/lesson/:lessonId",
queryParams: [],
usage: "Assigned course lesson player.",
},
},
usage: "Open catalog for general learning handoffs.",
@@ -481,7 +483,7 @@ const serviceRegistry: ServiceRecord[] = [
},
toolName: "prepare_course_handoff",
completionEvents: ["course.started", "course.completed"],
qscoreSignals: ["skills.learning", "readiness.upskilling"],
qscoreSignals: ["skills.learning", "readiness.course_progress"],
usage: "Use for skill-gap tasks that should become learning work.",
},
usageDocs: ["Call buildServiceLink('courses-service', 'catalog', state) for course handoffs."],
@@ -511,7 +513,7 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
assessment: {
path: "/upskilling/assessment",
path: "/agents/assessments",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Assessment landing and active assessment surface.",
},
@@ -560,18 +562,12 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
jobs: {
path: "/opportunities/job-matching",
aliases: ["/pathways"],
path: "/agents/matchmaking",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "query", "role", "location"],
usage: "Job/opportunity matching dashboard.",
},
pathways: {
path: "/career-pathways",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Career pathways dashboard and pathway list.",
},
},
usage: "Use jobs for immediate matching and pathways for career-plan handoffs.",
usage: "Use matchmaking for immediate opportunity matching and Scout-style recommendations.",
},
curator: {
defaultPage: "jobs",
@@ -587,79 +583,9 @@ const serviceRegistry: ServiceRecord[] = [
},
usageDocs: [
"Call buildServiceLink('matchmaking-service', 'jobs', state) for job matching.",
"Use pathways-service for generated career pathway plans.",
"Use the canonical /agents/matchmaking page for Scout-style opportunity review.",
],
},
{
id: "pathways-service",
label: "Pathways",
description: "Generate, activate, and report on personalized career pathways.",
category: "opportunity",
enabled: Boolean(config.pathwaysServiceUrl),
featureId: "pathways",
promptModulePath: "agents/pathways.md",
aliases: ["career-pathways-service"],
backend: {
baseUrl: config.pathwaysServiceUrl,
publicUrl: config.pathwaysPublicUrl,
healthPath: "/api/v1/health",
endpoints: {
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
state: endpoint("GET", "/api/state/:clerkId", "Reads pathway state.", "Hydrate pathway dashboard."),
profileIngest: endpoint("POST", "/api/v1/profiles/ingest", "Ingests a profile for pathway generation.", "Profile setup."),
questionnaire: endpoint("GET", "/api/v1/questionnaires/:userId", "Reads pathway questionnaire.", "Questionnaire resume."),
createQuestionnaire: endpoint("POST", "/api/v1/questionnaires", "Creates questionnaire answers.", "Pathway onboarding."),
generatePathway: endpoint("POST", "/api/v1/pathways/generate", "Generates a pathway.", "Career-plan generation."),
activatePathway: endpoint("POST", "/api/v1/pathways/:pathwayId/activate", "Activates a pathway.", "Commit chosen pathway."),
getPathway: endpoint("GET", "/api/v1/pathways/:pathwayId", "Reads pathway details.", "Pathway detail."),
weeklyPlan: endpoint("GET", "/api/v1/pathways/:pathwayId/weekly-plan", "Reads weekly plan.", "Planner UI."),
report: endpoint("GET", "/api/v1/pathways/:pathwayId/report", "Reads pathway report.", "Progress report."),
opportunities: endpoint("GET", "/api/v1/pathways/:pathwayId/opportunities", "Reads pathway opportunities.", "Pathway opportunity recommendations."),
a2aTask: endpoint("POST", "/a2a/tasks", "Runs pathway agent tasks.", "Agent/curator orchestrated work."),
},
usage: "Use for generated pathway plans and recommendation context. The live container is currently healthchecked separately from matchmaking.",
},
frontend: {
baseUrl: frontendBaseUrl,
pages: {
dashboard: {
path: "/career-pathways",
aliases: ["/pathways"],
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Pathway list/dashboard.",
},
generate: {
path: "/career-pathways/generate",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Pathway generation flow.",
},
questionnaire: {
path: "/career-pathways/questionnaire",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Pathway questionnaire flow.",
},
detail: {
path: "/career-pathways/dashboard/:pathwayId",
queryParams: [],
usage: "Pathway detail dashboard.",
},
},
usage: "Use dashboard for general pathway handoffs and generate/questionnaire for guided setup.",
},
curator: {
defaultPage: "dashboard",
defaultActionLabel: "Open pathways",
actionLabels: {
start: "Build pathway",
review: "Review pathway",
},
toolName: "prepare_pathways_handoff",
completionEvents: ["pathway.generated", "pathway.activated", "pathway.report_viewed"],
qscoreSignals: ["readiness.pathway", "market.plan"],
usage: "Use for strategic career pathway planning rather than immediate job matching.",
},
usageDocs: ["Call buildServiceLink('pathways-service', 'dashboard', state) for pathway handoffs."],
},
{
id: "qscore-service",
label: "QScore",
@@ -685,13 +611,12 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
dashboard: {
path: "/home",
aliases: ["/analytics"],
path: "/agents/qscore",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Home dashboard with QScore panel.",
usage: "Dedicated Q Score service page with live readiness signals.",
},
},
usage: "Open home dashboard for QScore review until a dedicated analytics route exists.",
usage: "Open the dedicated Q Score service page for score review.",
},
curator: {
defaultPage: "dashboard",
@@ -732,8 +657,7 @@ const serviceRegistry: ServiceRecord[] = [
baseUrl: frontendBaseUrl,
pages: {
profile: {
path: "/opportunities/social-media",
aliases: ["/social"],
path: "/agents/social-branding",
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
usage: "Social/profile improvement page.",
},

View File

@@ -1,4 +1,4 @@
import { and, desc, eq } from "drizzle-orm";
import { and, desc, eq, inArray } from "drizzle-orm";
import { db } from "../../db/client.js";
import { growEvents, growHomeNotifications, type GrowEventRow } from "../../db/schema.js";
import { asRecord, getString } from "../../events/envelope.js";
@@ -44,6 +44,17 @@ function parseCompletedAt(value: unknown): string | undefined {
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
}
function onboardingContextFromPayload(payload: Record<string, unknown>) {
const preferences = asRecord(payload.preferences);
const onboarding = asRecord(payload.onboarding ?? preferences.onboarding);
if (!Object.keys(onboarding).length && !Object.keys(preferences).length) return undefined;
return {
onboarding,
preferences: Object.keys(preferences).length ? preferences : { onboarding },
source: "grow_events",
};
}
export function onboardingCompletedAtFromPreferences(preferences: Record<string, unknown> | undefined) {
const onboarding = asRecord(preferences?.onboarding);
return parseCompletedAt(onboarding.completed_at ?? onboarding.completedAt);
@@ -123,6 +134,22 @@ async function recordOnboardingContextSnapshot(input: {
}, { userId: input.userId, source: input.source ?? "onboarding" });
}
async function findLatestOnboardingContext(userId: string) {
const [event] = await db
.select({ id: growEvents.id, payload: growEvents.payload, occurredAt: growEvents.occurredAt, source: growEvents.source, type: growEvents.type })
.from(growEvents)
.where(and(
eq(growEvents.userId, userId),
inArray(growEvents.type, ["onboarding.snapshot.saved", "onboarding.completed", "user.onboarding.completed", "profile.onboarding.completed"]),
))
.orderBy(desc(growEvents.occurredAt))
.limit(1);
if (!event?.payload) return undefined;
const context = onboardingContextFromPayload(event.payload);
return context ? { ...context, sourceEventId: event.id, sourceEventType: event.type, sourceEventSource: event.source } : undefined;
}
async function fetchUserServiceContext(userId: string): Promise<Record<string, unknown> | undefined> {
const token = config.serviceToken || (config.nodeEnv !== "production" ? config.a2aAllowedKey : "");
if (!token) return undefined;
@@ -228,7 +255,7 @@ export async function runCuratorOnboardingLoop(input: OnboardingLoopInput): Prom
}
const startDate = isoDateFrom(input.completedAt);
const context = input.context ?? await fetchUserServiceContext(userId);
const context = input.context ?? await findLatestOnboardingContext(userId) ?? await fetchUserServiceContext(userId);
await recordOnboardingContextSnapshot({
userId,
startDate,
@@ -272,6 +299,7 @@ export async function runCuratorOnboardingLoopForEvent(event: GrowEventRow): Pro
completedAt: onboardingCompletedAtFromEvent(event),
sourceEventId: event.id,
source: event.source,
context: onboardingContextFromPayload(event.payload ?? {}),
});
}

View File

@@ -4,6 +4,7 @@ import { eq, desc, and, inArray } from "drizzle-orm";
import { db } from "../../db/client.js";
import { growEvents, growQscoreLatest, growQscoreProjectionState } from "../../db/schema.js";
import { interviewService, resumeService, roleplayService } from "../../services/product-service-clients.js";
import { listServices } from "../../services/service-registry.js";
import { createMissionAction, listMissionActions } from "../../missions/actions.js";
import { listActiveMissionsPg, listMessagesPg } from "../../grow/persistence.js";
import { buildCuratorStreak, buildCuratorTasks, listCuratorRegistryCapabilities } from "./curator-store.js";
@@ -276,9 +277,9 @@ export function buildCuratorTools(ctx: { userId: string; date: string; conversat
execute: async ({ taskId }) => {
const task = taskId ? await findTask(ctx.userId, taskId, ctx.date) : null;
return {
routes: ["interview-service", "resume-service", "roleplay-service", "qscore-service"].map((serviceId) => ({
serviceId,
route: serviceRoute({ serviceId: serviceId as any, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }),
routes: listServices().map((service) => ({
serviceId: service.id,
route: serviceRoute({ serviceId: service.id, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }),
})),
};
},

View File

@@ -10,7 +10,6 @@ export const curatorServiceIdSchema = z.enum([
"qscore-service",
"social-branding-service",
"matchmaking-service",
"pathways-service",
]);
export type CuratorServiceId = z.infer<typeof curatorServiceIdSchema>;