Files
zopu-code/packages/backend/convex/signalRouting.ts
-Puter a5414d83bd Convex-only Slice 1: clients talk to Convex, Flue is a private worker
Normalize the topology so web/desktop/mobile clients communicate only with
Convex. Convex admits durable commands, dispatches private Flue turns through
FLUE_URL, and stores the product-facing result before clients observe it.

- Normalized relational schema: organizations, projects, conversations/turns/
  messages/attachments, signals/sources/constraints, works/events/attachments.
- Conversation turns queued in Convex; the agent runAction dispatches to Flue
  and persists assistant response, signals, and proposed work back into Convex.
- Browser chat moved off the Flue transport: use-chat-agent now reads reactive
  Convex rows and sends through conversationMessages.send.
- Fixed signed-in blank screen: gate all product queries on useConvexAuth
  (isAuthenticated && !isRefreshing) plus personal-org bootstrap.
- Pinned react/react-dom to 19.2.8 via catalog to fix SSR dual-React crash.
- Removed ~16.6k net lines: daemon, AgentOS/Orb, project issues/runs/artifacts,
  todos, browser Flue transport, mobile execution views, smoke scripts.

Verified end-to-end on cheaptricks: connect repo, send actionable message,
Flue turn completes, Signal + proposed Work created with exact provenance,
persists across refresh.
2026-07-27 21:32:17 +05:30

174 lines
5.2 KiB
TypeScript

import { env } from "@code/env/convex";
import { ConvexError, v } from "convex/values";
import type { Id } from "./_generated/dataModel";
import { mutation, query } from "./_generated/server";
const requireAgent = (token: string): void => {
if (token !== env.FLUE_DB_TOKEN) {
throw new ConvexError("Invalid agent control token");
}
};
export const listProjects = query({
args: { organizationId: v.id("organizations"), token: v.string() },
handler: async (ctx, args) => {
requireAgent(args.token);
const projects = await ctx.db
.query("projects")
.withIndex("by_organizationId_and_createdAt", (q) =>
q.eq("organizationId", args.organizationId)
)
.order("desc")
.take(50);
return projects.map((project) => ({
_id: project._id,
description: project.description ?? null,
name: project.name,
}));
},
});
export const listEvidence = query({
args: { organizationId: v.id("organizations"), token: v.string() },
handler: async (ctx, args) => {
requireAgent(args.token);
const conversation = await ctx.db
.query("conversations")
.withIndex("by_organizationId", (q) =>
q.eq("organizationId", args.organizationId)
)
.unique();
if (!conversation) {
return [];
}
const messages = await ctx.db
.query("conversationMessages")
.withIndex("by_conversationId_and_ordinal", (q) =>
q.eq("conversationId", conversation._id)
)
.order("desc")
.take(100);
const evidence: {
messageId: string;
rawText: string;
createdAt: number;
submissionId: string | null;
}[] = [];
for (const message of messages) {
if (message.role !== "user") {
continue;
}
const turn = await ctx.db.get(message.turnId);
if (!turn || !["processing", "completed"].includes(turn.status)) {
continue;
}
const consumed = await ctx.db
.query("signalSources")
.withIndex("by_messageId", (q) => q.eq("messageId", message._id))
.first();
if (!consumed) {
evidence.push({
createdAt: message.createdAt,
messageId: String(message._id),
rawText: message.content,
submissionId: turn.submissionId ?? null,
});
}
}
return evidence;
},
});
export const createSignal = mutation({
args: {
organizationId: v.id("organizations"),
projectId: v.id("projects"),
messageIds: v.array(v.string()),
problemStatement: v.object({
title: v.string(),
summary: v.string(),
desiredOutcome: v.string(),
constraints: v.array(v.string()),
}),
processedByAgentInstanceId: v.string(),
token: v.string(),
},
handler: async (ctx, args) => {
requireAgent(args.token);
const project = await ctx.db.get(args.projectId);
if (!project || project.organizationId !== args.organizationId) {
throw new ConvexError("Project not found");
}
const conversation = await ctx.db
.query("conversations")
.withIndex("by_organizationId", (q) =>
q.eq("organizationId", args.organizationId)
)
.unique();
if (!conversation || args.messageIds.length === 0) {
throw new ConvexError("Signal evidence is required");
}
const sourceKey = JSON.stringify(args.messageIds);
const existing = await ctx.db
.query("signals")
.withIndex("by_organization_and_sourceKey", (q) =>
q.eq("organizationId", args.organizationId).eq("sourceKey", sourceKey)
)
.unique();
if (existing) {
return { signalId: existing._id };
}
const messages = [];
for (const messageId of args.messageIds) {
const message = await ctx.db.get(messageId as Id<"conversationMessages">);
if (
!message ||
message.conversationId !== conversation._id ||
message.role !== "user"
) {
throw new ConvexError(`Source message not found: ${messageId}`);
}
const turn = await ctx.db.get(message.turnId);
if (!turn || !["processing", "completed"].includes(turn.status)) {
throw new ConvexError(`Source message is not admitted: ${messageId}`);
}
messages.push(message);
}
const signalId = await ctx.db.insert("signals", {
conversationId: conversation._id,
createdAt: Date.now(),
desiredOutcome: args.problemStatement.desiredOutcome,
organizationId: args.organizationId,
processedByAgentInstanceId: args.processedByAgentInstanceId,
processedByAgentName: "zopu",
projectId: args.projectId,
sourceKey,
summary: args.problemStatement.summary,
title: args.problemStatement.title,
});
for (const [
ordinal,
constraint,
] of args.problemStatement.constraints.entries()) {
await ctx.db.insert("signalConstraints", {
ordinal,
signalId,
value: constraint,
});
}
for (const [ordinal, message] of messages.entries()) {
await ctx.db.insert("signalSources", {
messageId: message._id,
ordinal,
rawTextSnapshot: message.content,
signalId,
sourceCreatedAt: message.createdAt,
});
}
return { signalId };
},
});