From 10478fb035170152b9ceb18a1e1aa4022b007e93 Mon Sep 17 00:00:00 2001 From: Sai-karthik Date: Mon, 29 Jun 2026 18:38:03 +0000 Subject: [PATCH] Fix staging home mission service routing --- src/home/home-feed-agent.ts | 25 +++++++-------- src/home/home-feed.ts | 52 ++++++++++++++------------------ src/services/service-registry.ts | 14 +++++++-- src/v1/curator/curator-store.ts | 29 ++++++++++++++---- 4 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/home/home-feed-agent.ts b/src/home/home-feed-agent.ts index b064015..c9496d2 100644 --- a/src/home/home-feed-agent.ts +++ b/src/home/home-feed-agent.ts @@ -31,8 +31,9 @@ const rawFeedSchema = z.object({ notifications: z.array(rawNotificationSchema).min(1).max(24), }); -const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 90000); -const HOME_FEED_AGENT_ATTEMPTS = Math.max(1, Number(process.env.HOME_FEED_AGENT_ATTEMPTS ?? 2)); +const HOME_FEED_AGENT_ENABLED = process.env.HOME_FEED_AGENT_ENABLED === "true"; +const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 5000); +const HOME_FEED_AGENT_ATTEMPTS = Math.max(1, Number(process.env.HOME_FEED_AGENT_ATTEMPTS ?? 1)); export type AgentHomeNotification = z.infer; @@ -50,7 +51,6 @@ Every notification must point to one of these real dashboard routes: - /agents/resume for resume building, resume analysis, ATS, resume suggestions - /agents/interview for mock interview setup, interview session, interview review - /agents/roleplay for recruiter/manager/salary/stakeholder roleplay -- /agents/qscore for Q Score/readiness explanations - /missions for mission progress, approvals, artifacts, next stages - /agents/social-branding for LinkedIn/social branding - /agents/matchmaking for Scout/opportunity matching @@ -58,7 +58,7 @@ Every notification must point to one of these real dashboard routes: - /suggestions for broad onboarding/profile suggestions Every notification object must include: - moduleId: one of ${MODULE_IDS.join(", ")} -- source: one of resume, interview, roleplay, qscore, mission, social, pathways, rewards, system +- source: one of resume, interview, roleplay, mission, social, pathways, rewards, system Use minimal iPhone-notification copy: title <= 72 chars, subtitle <= 110 chars, short tag <= 14 chars. Use urgency truthfully: now = needs immediate user action, today = useful today, soon = next few days, calm = informational.`; @@ -79,8 +79,7 @@ function sourceFromHref(href: string) { if (href.startsWith("/agents/resume")) return "resume"; if (href.startsWith("/agents/interview")) return "interview"; if (href.startsWith("/agents/roleplay")) return "roleplay"; - if (href.startsWith("/agents/qscore")) return "qscore"; - if (href.startsWith("/missions")) return "mission"; + if (href.startsWith("/missions")) return "mission"; if (href.startsWith("/agents/social-branding")) return "social"; if (href.startsWith("/agents/matchmaking")) return "pathways"; if (href.startsWith("/rewards")) return "rewards"; @@ -97,7 +96,6 @@ function moduleFromSource(source: NonNullable): } function tagFromSource(source: NonNullable) { - if (source === "qscore") return "Q Score"; if (source === "mission") return "Mission"; if (source === "roleplay") return "Roleplay"; if (source === "interview") return "Interview"; @@ -112,7 +110,6 @@ function defaultHrefForSource(source: NonNullable item.href === raw.href) : undefined) ?? seeds.find((item) => item.title.toLowerCase() === raw.title.toLowerCase()); const inferredSource = raw.source ?? seed?.source; - const moduleId = raw.moduleId ?? seed?.moduleId ?? (inferredSource ? moduleFromSource(inferredSource) : "suggestions"); - const rawHref = raw.href ?? seed?.href ?? (inferredSource ? defaultHrefForSource(inferredSource, moduleId) : `/${moduleId}`); + const normalizedSource = inferredSource === "qscore" ? "pathways" : inferredSource; + const moduleId = raw.moduleId ?? seed?.moduleId ?? (normalizedSource ? moduleFromSource(normalizedSource) : "suggestions"); + const rawHref = raw.href ?? seed?.href ?? (normalizedSource ? defaultHrefForSource(normalizedSource, moduleId) : `/${moduleId}`); const href = sanitizeHref(rawHref, moduleId); - const source = raw.source ?? seed?.source ?? sourceFromHref(href); + const seedSource = seed?.source === "qscore" ? "pathways" : seed?.source; + const source = raw.source ?? seedSource ?? sourceFromHref(href); return notificationSchema.parse({ ...raw, tag: raw.tag ?? seed?.tag ?? tagFromSource(source), @@ -157,7 +156,8 @@ function completeNotificationsWithSeeds( for (const seed of seeds) { if (completed.length >= 6) break; - const candidate = normalizeAgentNotification(seed, seeds); + const candidateSeed = seed.source === "qscore" ? { ...seed, source: "pathways" as const, href: seed.href?.startsWith("/agents/qscore") ? "/agents/matchmaking" : seed.href } : seed; + const candidate = normalizeAgentNotification(candidateSeed, seeds); const key = notificationKey(candidate); if (seen.has(key)) continue; completed.push(candidate); @@ -184,6 +184,7 @@ export async function refineHomeNotificationsWithAgent(input: { context: Record; seeds: Array & { moduleId: HomeModuleId }>; }): Promise> { + if (!HOME_FEED_AGENT_ENABLED) return []; if (!config.llmApiKey && config.nodeEnv === "production") { throw new HomeFeedAgentError("home_feed_agent_missing_llm_api_key"); } diff --git a/src/home/home-feed.ts b/src/home/home-feed.ts index c657e30..acf14ac 100644 --- a/src/home/home-feed.ts +++ b/src/home/home-feed.ts @@ -40,9 +40,7 @@ const SERVICE_HREFS = { 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") ?? "/agents/social-branding", pathways: buildServiceLink("matchmaking-service", "jobs") ?? "/agents/matchmaking", rewards: "/rewards", suggestions: "/suggestions", @@ -101,10 +99,10 @@ function profileFromPreferences(preferences: Record) { }; } -function serviceHref(service: "resume" | "interview" | "roleplay" | "qscore", ctx: HomeContext, mission?: { instanceId?: string; missionId?: string; stageId?: string | null }) { +function serviceHref(service: "resume" | "interview" | "roleplay", ctx: HomeContext, mission?: { instanceId?: string; missionId?: string; stageId?: string | null }) { const profile = profileFromPreferences(ctx.preferences); - const serviceId = service === "qscore" ? "qscore-service" : `${service}-service`; - const pageId = service === "resume" ? "workspace" : service === "qscore" ? "dashboard" : "setup"; + const serviceId = `${service}-service`; + const pageId = service === "resume" ? "workspace" : "setup"; return buildServiceLink(serviceId, pageId, { source: "home", missionInstanceId: mission?.instanceId, @@ -126,7 +124,7 @@ function sourceFromSuggestionRole(role: string): HomeSource { if (value.includes("resume")) return "resume"; if (value.includes("roleplay")) return "roleplay"; if (value.includes("interview")) return "interview"; - if (value.includes("q")) return "qscore"; + if (value.includes("match") || value.includes("pathway") || value.includes("job")) return "pathways"; return "mission"; } @@ -193,9 +191,8 @@ function buildDayOneSeeds(): SeedNotification[] { { id: "resume-service", moduleId: "productivity" as const, href: SERVICE_HREFS.resume, source: "resume" as const, urgency: "today" as const }, { id: "interview-service", moduleId: "productivity" as const, href: SERVICE_HREFS.interview, source: "interview" as const, urgency: "today" as const }, { id: "roleplay-service", moduleId: "productivity" as const, href: SERVICE_HREFS.roleplay, source: "roleplay" as const, urgency: "soon" as const }, - { id: "qscore-service", moduleId: "suggestions" as const, href: SERVICE_HREFS.qscore, source: "qscore" as const, urgency: "now" as const }, - { id: "social-branding-service", moduleId: "social" as const, href: SERVICE_HREFS.social, source: "social" as const, urgency: "soon" as const }, - { id: "matchmaking-service", moduleId: "pathways" as const, href: SERVICE_HREFS.pathways, source: "pathways" as const, urgency: "calm" as const }, + { id: "courses-service", moduleId: "productivity" as const, href: SERVICE_HREFS.productivity, source: "system" as const, urgency: "soon" as const }, + { id: "matchmaking-service", moduleId: "pathways" as const, href: SERVICE_HREFS.pathways, source: "pathways" as const, urgency: "today" as const }, ]; for (const [index, card] of serviceCards.entries()) { @@ -233,7 +230,14 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] { const roleplayReview = serviceEvent(ctx, "roleplay.", "review"); const resumeAnalysis = serviceEvent(ctx, "resume.", "analysis"); - for (const suggestion of ctx.missionSuggestions.slice(0, 5)) { + const visibleMissionSuggestions = ctx.missionSuggestions + .filter((suggestion) => { + const haystack = `${suggestion.role} ${suggestion.title} ${suggestion.body} ${suggestion.ctaLabel} ${suggestion.ctaHref}`.toLowerCase(); + return !haystack.includes("qscore") && !haystack.includes("q score") && !haystack.includes("assessment") && !haystack.includes("/agents/qscore"); + }) + .slice(0, 5); + + for (const suggestion of visibleMissionSuggestions) { const mission = ctx.activeMissions.find((item) => item.instanceId === suggestion.missionInstanceId); const source = sourceFromSuggestionRole(suggestion.role); const href = sanitizeHref(suggestion.ctaHref, mission ? `/missions/active?missionInstanceId=${encodeURIComponent(mission.instanceId)}` : SERVICE_HREFS.mission); @@ -276,13 +280,13 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] { if (ctx.qscore || ctx.qscoreSignals.length) { pushSeed(seeds, { - moduleId: "suggestions", - title: qscore >= 80 ? "Protect your Q Score momentum" : "Raise your Q Score next", - subtitle: qscore >= 80 ? `Readiness is trending at ${qscore}. Keep one proof action moving for ${profile.targetRole}.` : `Current estimate is ${qscore || 64}. Resume + mock practice are fastest for ${profile.targetRole}.`, - tag: "Q Score", + moduleId: "pathways", + title: qscore >= 80 ? "Review your best job matches" : "Find better-fit job matches", + subtitle: qscore >= 80 ? `Your profile signals are strong enough to compare matched roles for ${profile.targetRole}.` : `Use resume and interview signals to surface roles that fit ${profile.targetRole}.`, + tag: "Matches", urgency: qscore >= 80 ? "today" : "now", - href: serviceHref("qscore", ctx), - source: "qscore", + href: SERVICE_HREFS.pathways, + source: "pathways", priority: 95, }); } @@ -327,17 +331,6 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] { }); } - pushSeed(seeds, { - moduleId: "social", - title: "Turn proof into LinkedIn updates", - subtitle: ctx.artifacts.length ? `${ctx.artifacts.length} artifact${ctx.artifacts.length === 1 ? "" : "s"} can feed headline, featured, or post ideas.` : `Connect LinkedIn and use ${profile.targetRole} proof to improve your profile.`, - tag: ctx.artifacts.length ? "Proof" : "Setup", - urgency: ctx.artifacts.length ? "today" : "soon", - href: SERVICE_HREFS.social, - source: "social", - priority: 70, - }); - if (resumeAnalysis || resumeSession || ats !== undefined) { pushSeed(seeds, { moduleId: "productivity", @@ -381,7 +374,7 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] { } if (!ctx.activeMissions.length) { - pushSeed(seeds, { moduleId: "missions", title: "Start Interview-to-Offer", subtitle: `Bundle resume fit, mock practice, and Q Score deltas for ${profile.targetRole}.`, tag: "Begin", urgency: "today", href: "/missions/available", source: "mission", priority: 80 }); + pushSeed(seeds, { moduleId: "missions", title: "Start Interview-to-Offer", subtitle: `Bundle resume fit, mock practice, and matched roles for ${profile.targetRole}.`, tag: "Begin", urgency: "today", href: "/missions/available", source: "mission", priority: 80 }); } return seeds; @@ -399,7 +392,7 @@ async function collectContext(userId: string, input: { userProfile?: Record) { const frontendBaseUrl = config.workflowsDashboardUrl.replace(/\/$/, ""); +const allowedServiceIds = new Set([ + "interview-service", + "matchmaking-service", + "roleplay-service", + "courses-service", + "resume-service", +]); + const serviceRegistry: ServiceRecord[] = [ { id: "interview-service", @@ -837,12 +845,12 @@ export function normalizeServiceId(serviceId?: string | null): ServiceId | undef } export function listServices() { - return serviceRegistry; + return serviceRegistry.filter((service) => allowedServiceIds.has(service.id)); } export function getService(serviceId?: string | null) { const normalized = normalizeServiceId(serviceId); - return normalized ? serviceRegistry.find((service) => service.id === normalized) : undefined; + return normalized ? listServices().find((service) => service.id === normalized) : undefined; } export function getServiceBackend(serviceId?: string | null) { @@ -881,7 +889,7 @@ export function buildServiceLink(serviceId: string | undefined, pageId?: string, } export function listServicesForCatalog() { - return serviceRegistry.map((service) => ({ + return listServices().map((service) => ({ id: service.id, label: service.label, description: service.description, diff --git a/src/v1/curator/curator-store.ts b/src/v1/curator/curator-store.ts index cdac807..65a50c3 100644 --- a/src/v1/curator/curator-store.ts +++ b/src/v1/curator/curator-store.ts @@ -113,14 +113,17 @@ type ServiceCurationPreviewInput = { }; const QSCORE_TASK_SERVICE_REPLACEMENTS: Record = { - measurement: "assessment-service", + measurement: "matchmaking-service", proof: "resume-service", practice: "interview-service", recovery: "roleplay-service", }; function curatorAssignableServiceId(serviceId: CuratorServiceId, taskType: CuratorTaskType): CuratorServiceId { - return serviceId === "qscore-service" ? QSCORE_TASK_SERVICE_REPLACEMENTS[taskType] : serviceId; + if (serviceId === "qscore-service") return QSCORE_TASK_SERVICE_REPLACEMENTS[taskType]; + if (serviceId === "social-branding-service") return taskType === "practice" ? "roleplay-service" : "resume-service"; + if (serviceId === "assessment-service") return "matchmaking-service"; + return serviceId; } function qscoreFreeTaskCopy(input: { @@ -159,10 +162,12 @@ function qscoreFreeTaskCopy(input: { } return { - title: input.title.replace(/Q Score|QScore/gi, "readiness assessment"), - subtitle: input.subtitle.replace(/Q Score|QScore/gi, "the assessment service"), - cta: "Open assessment", - signals: input.signals.map((signal) => signal.replace(/q\s?score/gi, "assessment")), + title: input.title.replace(/Q Score|QScore/gi, "job matches"), + subtitle: input.subtitle + .replace(/Q Score|QScore/gi, "job matching") + .replace(/assessment service|assessment/gi, "job matching"), + cta: "View matches", + signals: input.signals.map((signal) => signal.replace(/q\s?score|assessment/gi, "job matching")), }; } @@ -877,6 +882,18 @@ function seed( ): TaskSeed { const assignedServiceId = curatorAssignableServiceId(serviceId, taskType); const copy = qscoreFreeTaskCopy({ serviceId, taskType, title, subtitle, cta, signals }); + if (serviceId === "social-branding-service") { + return { + taskType, + serviceId: assignedServiceId, + title: copy.title.replace(/social proof|social profile|public credibility|visibility/gi, "resume proof"), + subtitle: copy.subtitle.replace(/social proof|social profile|public credibility|visibility|public positioning/gi, "resume proof"), + effort, + qxImpact, + cta: "Open resume workspace", + signals: copy.signals.map((signal) => signal.replace(/social|public|visibility/gi, "resume proof")), + }; + } return { taskType, serviceId: assignedServiceId, title: copy.title, subtitle: copy.subtitle, effort, qxImpact, cta: copy.cta, signals: copy.signals }; }