Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a7ea2be
perf(onboarding): parallelize vendor/risk extraction, fire-and-forget…
Marfuen May 7, 2026
c41f213
perf(onboarding): parallelize risk + vendor matching in linkage step
Marfuen May 7, 2026
a02aeb7
perf(onboarding): switch linkage reranker from gpt-5-mini to gemini-3…
Marfuen May 7, 2026
d29f7b9
chore: upgrade AI SDK to v6/v3, switch reranker to gemini-3-flash via…
Marfuen May 7, 2026
e8d0580
chore: remove caBundleExtension from trigger config
Marfuen May 7, 2026
6ac5f37
fix(api): await convertToModelMessages in assistant-chat controller
Marfuen May 7, 2026
d9e067f
fix(onboarding): restore tracker progress for parallel + fire-and-for…
Marfuen May 7, 2026
a562830
fix(onboarding): handle pipeda and other framework conditionals in po…
Marfuen May 7, 2026
bff0b24
fix(onboarding): restore expandable sections in tracker during backgr…
Marfuen May 7, 2026
4b4a88d
fix(onboarding): use tasks.batchTrigger for policies to preserve pare…
Marfuen May 7, 2026
df813cd
fix(onboarding): restore concurrencyKey for policy batch trigger
Marfuen May 7, 2026
dcd783b
fix(onboarding): sync all tracker spinner animations
Marfuen May 7, 2026
29bcf37
perf(onboarding): raise policy queue concurrency from 5 to 15
Marfuen May 7, 2026
4e29a15
perf(onboarding): switch policy generation from gpt-5-mini to gemini-…
Marfuen May 7, 2026
41b8cc1
perf(onboarding): policies first, gemini for all LLM calls, non-block…
Marfuen May 7, 2026
faa8d0e
perf(onboarding): replace LLM policy generation with programmatic tem…
Marfuen May 7, 2026
5840046
fix(onboarding): switch tracker from useRealtimeRun to useRun with po…
Marfuen May 7, 2026
a3335b3
feat(onboarding): add targeted LLM pass to rewrite policy cue lines
Marfuen May 7, 2026
4eff65d
perf(onboarding): switch reranker to gemini-3.1-flash-lite-preview
Marfuen May 7, 2026
0d3a2dc
fix(onboarding): use tasks.batchTrigger for vendor/risk mitigations
Marfuen May 7, 2026
64a2da3
fix: switch onboarding status hooks from useRealtimeRun to useRun
Marfuen May 7, 2026
a57fced
feat(onboarding): split tracker into 6 steps with separate mitigation…
Marfuen May 7, 2026
b16e311
fix(onboarding): keep task alive for mitigations, redirect via metada…
Marfuen May 7, 2026
c67cbfe
fix(onboarding): show vendor/risk count on creation steps
Marfuen May 7, 2026
7dd06da
fix(onboarding): sequential triggerAndWait for mitigations
Marfuen May 7, 2026
27a7b8d
fix(onboarding): link completed assessments to treatment-plan tab
Marfuen May 7, 2026
cd50157
fix: address cubic review findings in template processor and linkage
Marfuen May 7, 2026
da3618d
fix(onboarding): fix currentStep mismatch and treat assessing as active
Marfuen May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"version": "0.0.1",
"author": "",
"dependencies": {
"@ai-sdk/anthropic": "^2.0.53",
"@ai-sdk/groq": "^2.0.32",
"@ai-sdk/openai": "^2.0.65",
"@ai-sdk/anthropic": "^3.0.75",
"@ai-sdk/groq": "^3.0.38",
"@ai-sdk/openai": "^3.0.62",
"@aws-sdk/client-acm": "^3.948.0",
"@aws-sdk/client-api-gateway": "^3.948.0",
"@aws-sdk/client-apigatewayv2": "^3.948.0",
Expand Down Expand Up @@ -88,7 +88,7 @@
"@upstash/redis": "^1.34.2",
"@upstash/vector": "^1.2.2",
"adm-zip": "^0.5.16",
"ai": "^5.0.60",
"ai": "^6.0.175",
"archiver": "^7.0.1",
"axios": "^1.12.2",
"better-auth": "^1.4.22",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/assistant-chat/assistant-chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Important:
const result = streamText({
model: openai('gpt-5'),
system: systemPrompt,
messages: convertToModelMessages(messages),
messages: await convertToModelMessages(messages),
tools,
stopWhen: stepCountIs(5),
});
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/policies/policies.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,7 @@ Keep responses helpful and focused on the policy editing task.`;
const result = streamText({
model: openai('gpt-5.5'),
system: systemPrompt,
messages: convertToModelMessages(messages),
messages: await convertToModelMessages(messages),
});

return result.pipeTextStreamToResponse(res);
Expand Down
16 changes: 9 additions & 7 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
"version": "0.1.0",
"type": "module",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.0",
"@ai-sdk/groq": "^3.0.0",
"@ai-sdk/openai": "^3.0.0",
"@ai-sdk/provider": "^3.0.0",
"@ai-sdk/react": "^3.0.0",
"@ai-sdk/rsc": "^2.0.0",
"@ai-sdk/anthropic": "^3.0.75",
"@ai-sdk/gateway": "^3.0.110",
"@ai-sdk/google": "^3.0.68",
"@ai-sdk/groq": "^3.0.38",
"@ai-sdk/openai": "^3.0.62",
"@ai-sdk/provider": "^3.0.10",
"@ai-sdk/react": "^3.0.177",
"@ai-sdk/rsc": "^2.0.175",
"@aws-sdk/client-ec2": "^3.948.0",
"@aws-sdk/client-lambda": "^3.948.0",
"@aws-sdk/client-s3": "3.1013.0",
Expand Down Expand Up @@ -84,7 +86,7 @@
"@vercel/sandbox": "^0.0.21",
"@vercel/sdk": "^1.7.1",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.116",
"ai": "^6.0.175",
"ai-elements": "^1.6.1",
"axios": "^1.9.0",
"better-auth": "^1.4.22",
Expand Down
459 changes: 137 additions & 322 deletions apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card';
import { ScrollArea } from '@trycompai/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@trycompai/ui/tabs';
import { Policy, Task } from '@db';
import { useRealtimeRun } from '@trigger.dev/react-hooks';
import { useRun } from '@trigger.dev/react-hooks';
import {
ArrowRight,
CheckCircle2,
Expand Down Expand Up @@ -49,8 +49,8 @@ export function ToDoOverview({
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const { run: onboardingRun } = useRealtimeRun(onboardingTriggerJobId || '', {
enabled: !!onboardingTriggerJobId,
const { run: onboardingRun } = useRun(onboardingTriggerJobId || '', {
refreshInterval: 1000,
});

const IN_PROGRESS_STATUSES = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useRealtimeRun } from '@trigger.dev/react-hooks';
import { useRun } from '@trigger.dev/react-hooks';
import { useMemo } from 'react';
import type { PolicyTailoringStatus } from '../../all/components/policy-tailoring-context';

Expand All @@ -20,8 +20,8 @@ export function usePolicyOnboardingStatus(
onboardingRunId: string | null | undefined,
) {
const shouldSubscribe = Boolean(onboardingRunId);
const { run } = useRealtimeRun(shouldSubscribe ? onboardingRunId! : '', {
enabled: shouldSubscribe,
const { run } = useRun(shouldSubscribe ? onboardingRunId! : '', {
refreshInterval: 1000,
});

const itemStatuses = useMemo<Record<string, PolicyTailoringStatus>>(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useRealtimeRun } from '@trigger.dev/react-hooks';
import { useRun } from '@trigger.dev/react-hooks';
import { Download, Loader2 } from 'lucide-react';
import * as React from 'react';
import { toast } from 'sonner';
Expand Down Expand Up @@ -31,8 +31,8 @@ export function PoliciesTable({ promises, onboardingRunId }: PoliciesTableProps)
const orgId = params.orgId as string;

const shouldSubscribeToRun = Boolean(onboardingRunId);
const { run } = useRealtimeRun(shouldSubscribeToRun ? onboardingRunId! : '', {
enabled: shouldSubscribeToRun,
const { run } = useRun(shouldSubscribeToRun ? onboardingRunId! : '', {
refreshInterval: 1000,
});

const policyStatuses = React.useMemo<PolicyStatusMap>(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useRealtimeRun } from '@trigger.dev/react-hooks';
import { useRun } from '@trigger.dev/react-hooks';
import { useMemo } from 'react';

export type OnboardingItemStatus = 'pending' | 'processing' | 'created' | 'assessing' | 'completed';
Expand All @@ -15,8 +15,8 @@ export function useOnboardingStatus(
itemType: 'risks' | 'vendors',
) {
const shouldSubscribe = Boolean(onboardingRunId);
const { run } = useRealtimeRun(shouldSubscribe ? onboardingRunId! : '', {
enabled: shouldSubscribe,
const { run } = useRun(shouldSubscribe ? onboardingRunId! : '', {
Comment thread
Marfuen marked this conversation as resolved.
refreshInterval: 1000,
});

const itemStatuses = useMemo<Record<string, OnboardingItemStatus>>(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createGatewayProvider } from '@ai-sdk/gateway';
import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
import type { LanguageModelV2 } from '@ai-sdk/provider';
import type { LanguageModelV3 } from '@ai-sdk/provider';
import type { JSONValue } from 'ai';

export async function getAvailableModels() {
Expand All @@ -10,7 +10,7 @@ export async function getAvailableModels() {
}

export interface ModelOptions {
model: LanguageModelV2;
model: LanguageModelV3;
providerOptions?: Record<string, Record<string, JSONValue>>;
headers?: Record<string, string>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ export function OnboardingStatus({ runId }: { runId: string }) {
useEffect(() => {
if (run?.status === 'COMPLETED') {
router.replace('/');
return;
}
}, [run?.status, router]);
const meta = run?.metadata as Record<string, unknown> | undefined;
if (meta?.readyForDashboard === true) {
router.replace('/');
}
}, [run?.status, run?.metadata, router]);

return (
<div className="flex flex-col items-center justify-center">
Expand Down
63 changes: 28 additions & 35 deletions apps/app/src/lib/embedding/run-linkage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,10 @@ const AUTONOMOUS_FINAL_TOP_K = 8;
const AUTONOMOUS_MIN_RERANK_SCORE = 5;
const AUTONOMOUS_MIN_LINKS_FLOOR = 3;

// How many risks/vendors to match concurrently in the bulk onboarding path.
// Each iteration makes 1 vector query (Upstash) + 1 OpenAI rerank call + 1
// Prisma update — typical wall-clock per iteration is 3–10 seconds (the
// rerank LLM call dominates). With 32 in-flight at once a 20-entity
// onboarding finishes in roughly one batch, well within gpt-5-mini /
// Upstash rate limits.
//
// NOTE: this is in-process concurrency on a single trigger.dev task. The
// natural next step (true fan-out per entity using `task.batchTrigger`)
// would unlock trigger.dev's queue-level concurrency (50), but requires
// passing the embedded-task metadata to children rather than rebuilding
// taskById per child. Filed as a follow-up.
const MATCH_CONCURRENCY = 32;
// Risk and vendor matching run in parallel, so this limit applies to
// EACH side independently. Keep it at 16 so both sides combined stay
// under ~32 concurrent LLM rerank calls.
const MATCH_CONCURRENCY = 16;

async function mapWithConcurrency<T, R>(
items: T[],
Expand Down Expand Up @@ -604,14 +595,22 @@ export async function runLinkage({

let suggestions: RunLinkageOutput['suggestions'];

// Risk matching — fan out with bounded concurrency. Each iteration is one
// vector query + (rerank | DB update). Order doesn't matter here; we sum
// riskLinks at the end and emit `current` based on completion count.
// Emit initial matching phases before the parallel fan-out so the UI shows
// both phases starting at once.
if (risks.length > 0) {
onPhase?.({ name: 'matching-risks', current: 0, total: risks.length });
}
if (vendors.length > 0) {
onPhase?.({ name: 'matching-vendors', current: 0, total: vendors.length });
}

// Risk + vendor matching run in parallel — they write to separate DB
// tables (Risk.tasks vs Vendor.tasks) and the shared taskById is read-only.
let completedRisks = 0;
const riskOutcomes = await mapWithConcurrency(risks, MATCH_CONCURRENCY, async (risk) => {
let completedVendors = 0;

const [riskOutcomes, vendorOutcomes] = await Promise.all([
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
mapWithConcurrency(risks, MATCH_CONCURRENCY, async (risk) => {
const similar = await findSimilarTasks({
organizationId,
queryText: riskQueryText(risk),
Expand Down Expand Up @@ -687,23 +686,8 @@ export async function runLinkage({
completedRisks += 1;
onPhase?.({ name: 'matching-risks', current: completedRisks, total: risks.length });
return { count, perEntitySuggestions };
});
const riskLinks = riskOutcomes.reduce((sum, r) => sum + r.count, 0);
// suggestionsOnly endpoints always pass a single riskId, so at most one
// outcome carries suggestions — pick the first non-null.
for (const r of riskOutcomes) {
if (r.perEntitySuggestions) {
suggestions = r.perEntitySuggestions;
break;
}
}

// Vendor matching — same pattern.
if (vendors.length > 0) {
onPhase?.({ name: 'matching-vendors', current: 0, total: vendors.length });
}
let completedVendors = 0;
const vendorOutcomes = await mapWithConcurrency(vendors, MATCH_CONCURRENCY, async (vendor) => {
}),
mapWithConcurrency(vendors, MATCH_CONCURRENCY, async (vendor) => {
const similar = await findSimilarTasks({
organizationId,
queryText: vendorQueryText(vendor),
Expand Down Expand Up @@ -751,8 +735,17 @@ export async function runLinkage({
completedVendors += 1;
onPhase?.({ name: 'matching-vendors', current: completedVendors, total: vendors.length });
return { count, perEntitySuggestions };
});
}),
]);

const riskLinks = riskOutcomes.reduce((sum, r) => sum + r.count, 0);
const vendorLinks = vendorOutcomes.reduce((sum, v) => sum + v.count, 0);
for (const r of riskOutcomes) {
if (r.perEntitySuggestions) {
suggestions = r.perEntitySuggestions;
break;
}
}
if (!suggestions) {
for (const v of vendorOutcomes) {
if (v.perEntitySuggestions) {
Expand Down
10 changes: 7 additions & 3 deletions apps/app/src/lib/rerank-suggestions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { openai } from '@ai-sdk/openai';
import { createGatewayProvider } from '@ai-sdk/gateway';
import { generateObject, jsonSchema } from 'ai';

/**
Expand Down Expand Up @@ -39,7 +39,11 @@ export interface RerankedCandidate {
rerankScore: number;
}

const RERANK_MODEL = 'gpt-5-mini';
const gateway = createGatewayProvider({
baseURL: process.env.AI_GATEWAY_BASE_URL,
});

const RERANK_MODEL = 'google/gemini-3.1-flash-lite-preview' as const;

const SYSTEM_PROMPT = `You are a GRC analyst evaluating which compliance tasks would meaningfully reduce a specific risk or vendor exposure.

Expand Down Expand Up @@ -105,7 +109,7 @@ export async function rerankSuggestions({
.join('\n');

const result = await generateObject({
model: openai(RERANK_MODEL),
model: gateway(RERANK_MODEL),
system: SYSTEM_PROMPT,
prompt: userPrompt,
schema: rerankSchema,
Expand Down
28 changes: 21 additions & 7 deletions apps/app/src/trigger/lib/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { FrameworkEditorFramework, FrameworkEditorPolicyTemplate } from '@db';
import { logger } from '@trigger.dev/sdk';

const FRAMEWORK_MATCHERS: Array<{ flag: string; test: (name: string) => boolean }> = [
{ flag: 'soc2', test: (n) => /soc\s*2/i.test(n) || n.includes('soc') },
{ flag: 'hipaa', test: (n) => n.includes('hipaa') },
{ flag: 'pipeda', test: (n) => n.includes('pipeda') },
{ flag: 'gdpr', test: (n) => n.includes('gdpr') },
{ flag: 'iso27001', test: (n) => /iso\s*27001/i.test(n) },
{ flag: 'pci', test: (n) => /pci/i.test(n) },
{ flag: 'nist', test: (n) => /nist/i.test(n) },
{ flag: 'ccpa', test: (n) => n.includes('ccpa') },
];

function buildFrameworkFlags(frameworks: FrameworkEditorFramework[]): string {
return FRAMEWORK_MATCHERS.map(({ flag, test }) => {
const active = frameworks.some((f) => test(f.name.toLowerCase()));
return ` - ${flag} is ${active ? 'true' : 'false'}`;
}).join('\n');
}

export const generatePrompt = ({
policyTemplate,
contextHub,
Expand Down Expand Up @@ -29,10 +47,7 @@ export const generatePrompt = ({
frameworks.length > 0
? frameworks.map((f) => `${f.name} v${f.version}`).join(', ')
: 'None explicitly selected';
const hasHIPAA = frameworks.some((f) => f.name.toLowerCase().includes('hipaa'));
const hasSOC2 = frameworks.some(
(f) => /soc\s*2/i.test(f.name) || f.name.toLowerCase().includes('soc'),
);
const frameworkFlags = buildFrameworkFlags(frameworks);

return `
Company: ${companyName} (${companyWebsite})
Expand Down Expand Up @@ -67,10 +82,9 @@ Required rules (keep this simple):
- Do NOT copy instruction cue lines (e.g., "Add a HIPAA checklist...", "State that...", "Clarify that..."). Convert such cues into real policy language, and then remove the cue line entirely. If a cue precedes bullet points, keep the bullets but delete the cue line.

3) Handlebars-style conditionals
- The template may contain conditional blocks using {{#if var}}...{{/if}} syntax (e.g., {{#if soc2}}, {{#if hipaa}}).
- The template may contain conditional blocks using {{#if var}}...{{/if}} syntax (e.g., {{#if soc2}}, {{#if hipaa}}, {{#if pipeda}}).
- Evaluate these using the selected frameworks:
- soc2 is ${hasSOC2 ? 'true' : 'false'}
- hipaa is ${hasHIPAA ? 'true' : 'false'}
${frameworkFlags}
- If the condition is true: keep only the inner content and remove the {{#if}}/{{/if}} markers.
- If the condition is false: remove the entire block including its content.
- For any other unknown {{#if X}} variables: assume false and remove the block.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RiskStatus, db } from '@db/server';
import { logger, metadata, queue, tags, task } from '@trigger.dev/sdk';
import { logger, metadata, queue, tags, task, tasks } from '@trigger.dev/sdk';
import axios from 'axios';
import {
createRiskMitigationComment,
Expand Down Expand Up @@ -114,15 +114,16 @@ export const generateRiskMitigationsForOrg = task({

const policies = policyRows.map((p) => ({ name: p.name, description: p.description }));

await generateRiskMitigation.batchTrigger(
await tasks.batchTriggerAndWait<typeof generateRiskMitigation>(
'generate-risk-mitigation',
risks.map((r) => ({
payload: {
organizationId,
riskId: r.id,
authorId: author?.id,
policies,
},
concurrencyKey: `${organizationId}:${r.id}`,
options: { concurrencyKey: `${organizationId}:${r.id}` },
})),
);

Expand Down
Loading
Loading