Files
zopu-code/packages/backend/convex/signalRouting.ts
-Puter a8d946b6a9 Slice 4 control-plane repairs, lint/catalog cleanup, Flue adapter fixes
Convex control plane (Slices 1-4 prerequisites for Slice 5):
- Versioned Definition/Design persistence: revisions preserve history
  instead of deleting prior slices/approvals
- Fenced conversation turn queue: attempt-numbered leases prevent stale
  worker overwrites; expired turns reconciled by cron
- Fenced Run/Attempt claiming: only queued attempts from running Runs can
  be claimed; lease expiry checks on every finish/checkpoint
- Multi-slice progression: successful slice marks next slice ready instead
  of completing the whole Work; completed Runs blocked from retry
- Typed AttemptClassification in schema and resolver decisions table
- Durable resolver decisions for normal outcomes, cancellation, and
  lease-expiry recovery
- Persistent artifacts and delivery metadata with provenance, source
  revision, verification status, and controlled delivery transitions
- High-impact open questions block definition approval
- Question submission gated to defining/awaiting-approval states only
- Delivery status updates validate JSON metadata before persisting
- Planner failures recorded as durable blocked events
- Approval identity uses canonical tokenIdentifier

Primitives:
- decodeDefinition separates decode from validate
- Design validation enforces 1-4 slices with unique IDs and valid deps
- Artifact and delivery draft schemas with verified-revision binding
- Delivery status transition table

Package management:
- Consolidated shared deps into single catalog (hono, valibot, streamdown,
  @types/react, @types/node, @tailwindcss/*, react-native)
- Removed cross-version Hono boundary: flue() exported as Fetchable
- Deleted bun.lock and regenerated from clean catalog resolution

Lint/format:
- Removed .eslintignore; oxc uses native ignorePatterns in oxlint.config.ts
- Deleted redundant packages/backend/.oxlintrc.json
- Added repos/** and scripts/** to ignore patterns
- Convex-specific rule overrides for ES2022 target constraints
- Fixed all oxc errors in fluePersistence.ts and agents/src/db.ts
- Fixed all formatting across changed files
2026-07-28 02:53:05 +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: {
messageIds: v.array(v.string()),
organizationId: v.id("organizations"),
problemStatement: v.object({
constraints: v.array(v.string()),
desiredOutcome: v.string(),
summary: v.string(),
title: v.string(),
}),
processedByAgentInstanceId: v.string(),
projectId: v.id("projects"),
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 };
},
});