Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"typescript": "^5.9.3",
"wrangler": "^4.40.3"
},
"engines": {
Expand Down
73 changes: 71 additions & 2 deletions apps/mesh/src/storage/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ export class OrgScopedThreadStorage {

list(
createdBy?: string,
options?: { limit?: number; offset?: number; virtualMcpId?: string },
options?: {
limit?: number;
offset?: number;
virtualMcpId?: string;
startDate?: string;
endDate?: string;
search?: string;
status?: string;
agentId?: string;
},
): Promise<{ threads: Thread[]; total: number }> {
return this.inner.list(this.requireOrg(), createdBy, options);
}
Expand Down Expand Up @@ -237,7 +246,16 @@ export class SqlThreadStorage implements ThreadStoragePort {
async list(
organizationId: string,
createdBy?: string,
options?: { limit?: number; offset?: number; virtualMcpId?: string },
options?: {
limit?: number;
offset?: number;
virtualMcpId?: string;
startDate?: string;
endDate?: string;
search?: string;
status?: string;
agentId?: string;
},
): Promise<{ threads: Thread[]; total: number }> {
let query = this.db
.selectFrom("threads")
Expand All @@ -252,6 +270,30 @@ export class SqlThreadStorage implements ThreadStoragePort {
if (options?.virtualMcpId) {
query = query.where("virtual_mcp_id", "=", options.virtualMcpId);
}
if (options?.startDate) {
// updated_at is stored as ISO text — string comparison is correct for ISO dates
query = query.where(
"updated_at",
">=",
options.startDate as unknown as Date,
);
}
if (options?.endDate) {
query = query.where(
"updated_at",
"<=",
options.endDate as unknown as Date,
);
}
if (options?.search) {
query = query.where("title", "ilike", `%${options.search}%`);
}
if (options?.status) {
query = query.where("status", "=", options.status as ThreadStatus);
}
if (options?.agentId) {
query = query.where("virtual_mcp_id", "=", options.agentId);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Agent filtering is applied to virtual_mcp_id instead of agent_ids, so the agentId filter can return incorrect/no thread results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/threads.ts, line 295:

<comment>Agent filtering is applied to `virtual_mcp_id` instead of `agent_ids`, so the `agentId` filter can return incorrect/no thread results.</comment>

<file context>
@@ -292,8 +292,7 @@ export class SqlThreadStorage implements ThreadStoragePort {
     if (options?.agentId) {
-      // agent_ids is stored as a JSON text array — match the quoted ID inside it
-      query = query.where("agent_ids", "like", `%"${options.agentId}"%`);
+      query = query.where("virtual_mcp_id", "=", options.agentId);
     }
 
</file context>
Fix with Cubic

}

let countQuery = this.db
.selectFrom("threads")
Expand All @@ -269,6 +311,33 @@ export class SqlThreadStorage implements ThreadStoragePort {
options.virtualMcpId,
);
}
if (options?.startDate) {
countQuery = countQuery.where(
"updated_at",
">=",
options.startDate as unknown as Date,
);
}
if (options?.endDate) {
countQuery = countQuery.where(
"updated_at",
"<=",
options.endDate as unknown as Date,
);
}
if (options?.search) {
countQuery = countQuery.where("title", "ilike", `%${options.search}%`);
}
if (options?.status) {
countQuery = countQuery.where(
"status",
"=",
options.status as ThreadStatus,
);
}
if (options?.agentId) {
countQuery = countQuery.where("virtual_mcp_id", "=", options.agentId);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Count query applies agentId to virtual_mcp_id, producing incorrect totals for agent-filtered thread lists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/threads.ts, line 339:

<comment>Count query applies `agentId` to `virtual_mcp_id`, producing incorrect totals for agent-filtered thread lists.</comment>

<file context>
@@ -337,11 +336,7 @@ export class SqlThreadStorage implements ThreadStoragePort {
-        "like",
-        `%"${options.agentId}"%`,
-      );
+      countQuery = countQuery.where("virtual_mcp_id", "=", options.agentId);
     }
 
</file context>
Fix with Cubic

}

if (options?.limit) {
query = query.limit(options.limit);
Expand Down
34 changes: 33 additions & 1 deletion apps/mesh/src/tools/thread/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ const ThreadListInputSchema = CollectionListInputSchema.extend({
virtual_mcp_id: z.string().optional(),
})
.optional(),
startDate: z
.string()
.datetime()
.optional()
.describe("Filter threads updated at or after this ISO timestamp"),
endDate: z
.string()
.datetime()
.optional()
.describe("Filter threads updated at or before this ISO timestamp"),
search: z
.string()
.optional()
.describe("Full-text search on thread title (case-insensitive)"),
status: z
.string()
.optional()
.describe("Filter by thread status (e.g. completed, failed, in_progress)"),
userId: z
.string()
.optional()
.describe("Filter by the user who created the thread"),
agentId: z
.string()
.optional()
.describe("Filter by agent (connection or virtual MCP) ID"),
});

/**
Expand Down Expand Up @@ -58,7 +84,8 @@ export const COLLECTION_THREADS_LIST = defineTool({
const virtualMcpId = input.where?.virtual_mcp_id;
// "me" is a reserved value meaning "filter by the authenticated user"
const createdBy =
input.where?.created_by === "me" ? userId : input.where?.created_by;
input.userId ??
(input.where?.created_by === "me" ? userId : input.where?.created_by);

const { threads, total } = triggerIds?.length
? await ctx.storage.threads.listByTriggerIds(triggerIds, {
Expand All @@ -69,6 +96,11 @@ export const COLLECTION_THREADS_LIST = defineTool({
limit,
offset,
virtualMcpId,
startDate: input.startDate,
endDate: input.endDate,
search: input.search,
status: input.status,
agentId: input.agentId,
});

const hasMore = offset + limit < total;
Expand Down
12 changes: 12 additions & 0 deletions apps/mesh/src/tools/thread/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ export const ThreadEntitySchema = z.object({
.string()
.nullable()
.describe("User ID who last updated the thread"),
virtual_mcp_id: z
.string()
.optional()
.describe("Virtual MCP (agent) this thread was initiated with"),
// Typed as a loose record to stay compatible with the Kysely storage type
// (Thread.run_config: Record<string, unknown> | null). Callers that need the
// typed shape should parse with PersistedRunConfigSchema from run-config.ts.
run_config: z
.record(z.string(), z.unknown())
.nullable()
.optional()
.describe("Persisted run configuration (contains agent and model info)"),
});

export type ThreadEntity = z.infer<typeof ThreadEntitySchema>;
Expand Down
4 changes: 4 additions & 0 deletions apps/mesh/src/web/components/chat/chat-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,10 @@ export function useChatStream(): ChatStreamContextValue {
return ctx;
}

export function useOptionalChatStream(): ChatStreamContextValue | null {
return useContext(ChatStreamCtx);
}

export function useChatTask(): ChatTaskContextValue {
const ctx = useContext(ChatTaskCtx);
if (!ctx)
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/web/components/chat/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
ActiveTaskProvider,
useChatTask,
useChatStream,
useOptionalChatStream,
useChatPrefs,
useChatBridge,
type ChatStreamContextValue,
Expand Down
4 changes: 2 additions & 2 deletions apps/mesh/src/web/components/chat/message/assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { SmartAutoScroll } from "./smart-auto-scroll.tsx";
import { type DataParts, useFilterParts } from "./use-filter-parts.ts";
import { addUsage, emptyUsageStats } from "@decocms/mesh-sdk";
import { useChatStream } from "../context.tsx";
import { useOptionalChatStream } from "../context.tsx";

type ThinkingStage = "planning" | "thinking";

Expand Down Expand Up @@ -374,7 +374,7 @@ export function MessageAssistant({
className,
isLast = false,
}: MessageAssistantProps) {
const { isRunInProgress } = useChatStream();
const { isRunInProgress = false } = useOptionalChatStream() ?? {};
const isStreaming = status === "streaming";
const isSubmitted = status === "submitted";
const isLoading = isStreaming || isSubmitted;
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/src/web/components/monitoring/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export interface MonitoringLogsResponse

export interface MonitoringSearchParams {
// Tab selection
tab?: "overview" | "audit" | "dashboards";
tab?: "overview" | "audit" | "dashboards" | "threads";
// Time range using expressions (from/to)
from?: string; // e.g., "now-24h", "now-7d", or ISO string
to?: string; // e.g., "now" or ISO string
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/src/web/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ const monitoringRoute = createRoute({
component: lazyRouteComponent(() => import("./routes/orgs/monitoring.tsx")),
validateSearch: z.lazy(() =>
z.object({
tab: z.enum(["overview", "audit"]).default("overview"),
tab: z.enum(["overview", "audit", "threads"]).default("overview"),
from: z.string().default("now-30m"),
to: z.string().default("now"),
connectionId: z.array(z.string()).optional().default([]),
Expand Down
8 changes: 8 additions & 0 deletions apps/mesh/src/web/lib/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ export const KEYS = {
monitoringLogsInfinite: (locator: string, paramsKey: string) =>
["monitoring", "logs-infinite", locator, paramsKey] as const,

// Thread queries (scoped by locator)
threadsInfinite: (locator: string, paramsKey: string) =>
["threads", "list-infinite", locator, paramsKey] as const,
threadMessages: (locator: string, threadId: string) =>
["threads", "messages", locator, threadId] as const,
threadModelLogs: (locator: string, dateKey: string) =>
["threads", "model-logs", locator, dateKey] as const,

// Virtual MCP prompts (for ice breakers in chat)
// null virtualMcpId means default virtual MCP
virtualMcpPrompts: (virtualMcpId: string | null, orgId: string) =>
Expand Down
Loading
Loading