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.
This commit is contained in:
@@ -2,192 +2,6 @@ import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export default defineSchema({
|
||||
daemonDefinitions: defineTable({
|
||||
daemonId: v.string(),
|
||||
name: v.string(),
|
||||
desiredState: v.union(v.literal("online"), v.literal("offline")),
|
||||
agentOsConfig: v.any(),
|
||||
configVersion: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_daemonId", ["daemonId"]),
|
||||
daemonPresence: defineTable({
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
status: v.union(
|
||||
v.literal("online"),
|
||||
v.literal("draining"),
|
||||
v.literal("offline")
|
||||
),
|
||||
hostname: v.string(),
|
||||
platform: v.string(),
|
||||
architecture: v.string(),
|
||||
version: v.string(),
|
||||
startedAt: v.number(),
|
||||
lastHeartbeatAt: v.number(),
|
||||
})
|
||||
.index("by_daemonId", ["daemonId"])
|
||||
.index("by_sessionId", ["sessionId"]),
|
||||
daemonCommands: defineTable({
|
||||
daemonId: v.string(),
|
||||
method: v.string(),
|
||||
args: v.any(),
|
||||
actorKey: v.array(v.string()),
|
||||
status: v.union(
|
||||
v.literal("queued"),
|
||||
v.literal("claimed"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("cancelled")
|
||||
),
|
||||
attempts: v.number(),
|
||||
claimedBySessionId: v.optional(v.string()),
|
||||
leaseExpiresAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
startedAt: v.optional(v.number()),
|
||||
completedAt: v.optional(v.number()),
|
||||
result: v.optional(v.any()),
|
||||
error: v.optional(v.string()),
|
||||
})
|
||||
.index("by_daemonId_and_status", ["daemonId", "status"])
|
||||
.index("by_status_and_leaseExpiresAt", ["status", "leaseExpiresAt"]),
|
||||
daemonCommandEvents: defineTable({
|
||||
daemonId: v.string(),
|
||||
commandId: v.optional(v.id("daemonCommands")),
|
||||
kind: v.string(),
|
||||
data: v.any(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_daemonId_and_createdAt", ["daemonId", "createdAt"])
|
||||
.index("by_commandId_and_createdAt", ["commandId", "createdAt"]),
|
||||
projects: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
name: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_organization_and_createdAt", ["organizationId", "createdAt"]),
|
||||
projectSources: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
kind: v.literal("git"),
|
||||
url: v.string(),
|
||||
normalizedUrl: v.string(),
|
||||
host: v.string(),
|
||||
repositoryPath: v.string(),
|
||||
defaultBranch: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_project", ["projectId"])
|
||||
.index("by_organization_and_normalizedUrl", [
|
||||
"organizationId",
|
||||
"normalizedUrl",
|
||||
])
|
||||
.index("by_organization_and_createdAt", ["organizationId", "createdAt"]),
|
||||
projectContextDocuments: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
kind: v.union(
|
||||
v.literal("readme"),
|
||||
v.literal("agents"),
|
||||
v.literal("product"),
|
||||
v.literal("business"),
|
||||
v.literal("design"),
|
||||
v.literal("tech")
|
||||
),
|
||||
path: v.string(),
|
||||
content: v.string(),
|
||||
revision: v.number(),
|
||||
origin: v.union(
|
||||
v.literal("repository"),
|
||||
v.literal("paste"),
|
||||
v.literal("upload"),
|
||||
v.literal("public-url")
|
||||
),
|
||||
sourceUrl: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_project", ["projectId"])
|
||||
.index("by_project_and_kind", ["projectId", "kind"])
|
||||
.index("by_project_and_path", ["projectId", "path"]),
|
||||
projectArtifacts: defineTable({
|
||||
projectId: v.id("projects"),
|
||||
path: v.string(),
|
||||
content: v.string(),
|
||||
revision: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_project", ["projectId"])
|
||||
.index("by_project_and_path", ["projectId", "path"]),
|
||||
projectIssues: defineTable({
|
||||
projectId: v.id("projects"),
|
||||
number: v.number(),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
status: v.union(
|
||||
v.literal("open"),
|
||||
v.literal("queued"),
|
||||
v.literal("working"),
|
||||
v.literal("needs-input"),
|
||||
v.literal("completed"),
|
||||
v.literal("failed")
|
||||
),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_project_and_number", ["projectId", "number"])
|
||||
.index("by_project_and_status", ["projectId", "status"]),
|
||||
projectWorkRuns: defineTable({
|
||||
baseBranch: v.optional(v.string()),
|
||||
branchName: v.optional(v.string()),
|
||||
checkoutPath: v.optional(v.string()),
|
||||
projectId: v.id("projects"),
|
||||
issueId: v.id("projectIssues"),
|
||||
agentId: v.string(),
|
||||
daemonId: v.string(),
|
||||
actorKey: v.array(v.string()),
|
||||
sourceUrl: v.optional(v.string()),
|
||||
status: v.union(
|
||||
v.literal("ready"),
|
||||
v.literal("queued"),
|
||||
v.literal("working"),
|
||||
v.literal("needs-input"),
|
||||
v.literal("completed"),
|
||||
v.literal("failed")
|
||||
),
|
||||
summary: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
startedAt: v.optional(v.number()),
|
||||
completedAt: v.optional(v.number()),
|
||||
})
|
||||
.index("by_issue", ["issueId"])
|
||||
.index("by_project_and_createdAt", ["projectId", "createdAt"]),
|
||||
projectEvents: defineTable({
|
||||
projectId: v.id("projects"),
|
||||
issueId: v.optional(v.id("projectIssues")),
|
||||
runId: v.optional(v.id("projectWorkRuns")),
|
||||
kind: v.string(),
|
||||
data: v.any(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_project_and_createdAt", ["projectId", "createdAt"])
|
||||
.index("by_issue_and_createdAt", ["issueId", "createdAt"]),
|
||||
todos: defineTable({
|
||||
text: v.string(),
|
||||
completed: v.boolean(),
|
||||
}),
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Organization tenancy. Organizations are hard tenant boundaries; deny by
|
||||
// default. The membership user ID is the Better Auth Convex identity
|
||||
// `tokenIdentifier`.
|
||||
// -----------------------------------------------------------------
|
||||
organizations: defineTable({
|
||||
name: v.string(),
|
||||
kind: v.union(v.literal("personal"), v.literal("team")),
|
||||
@@ -203,58 +17,86 @@ export default defineSchema({
|
||||
.index("by_userId", ["userId"])
|
||||
.index("by_organizationId", ["organizationId"])
|
||||
.index("by_organizationId_and_userId", ["organizationId", "userId"]),
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Normalized conversation message evidence. Each row is one user
|
||||
// message captured at the conversation ingress, before/with Flue
|
||||
// admission. The raw text is stored exactly as the user typed it
|
||||
// (never trimmed or rewritten). Organization scope is authoritative;
|
||||
// an agent instance may only observe its own organization's messages.
|
||||
// -----------------------------------------------------------------
|
||||
conversationMessages: defineTable({
|
||||
projects: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
conversationId: v.string(),
|
||||
messageId: v.string(),
|
||||
submissionId: v.optional(v.string()),
|
||||
name: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
sourceUrl: v.string(),
|
||||
normalizedSourceUrl: v.string(),
|
||||
sourceHost: v.string(),
|
||||
repositoryPath: v.string(),
|
||||
defaultBranch: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_organizationId_and_createdAt", ["organizationId", "createdAt"])
|
||||
.index("by_organizationId_and_normalizedSourceUrl", [
|
||||
"organizationId",
|
||||
"normalizedSourceUrl",
|
||||
]),
|
||||
projectContextDocuments: defineTable({
|
||||
projectId: v.id("projects"),
|
||||
kind: v.union(
|
||||
v.literal("readme"),
|
||||
v.literal("agents"),
|
||||
v.literal("product"),
|
||||
v.literal("business"),
|
||||
v.literal("design"),
|
||||
v.literal("tech")
|
||||
),
|
||||
path: v.string(),
|
||||
content: v.string(),
|
||||
origin: v.literal("repository"),
|
||||
sourceUrl: v.string(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_projectId_and_path", ["projectId", "path"]),
|
||||
conversations: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
createdAt: v.number(),
|
||||
}).index("by_organizationId", ["organizationId"]),
|
||||
conversationTurns: defineTable({
|
||||
conversationId: v.id("conversations"),
|
||||
clientRequestId: v.string(),
|
||||
role: v.literal("user"),
|
||||
rawText: v.string(),
|
||||
status: v.union(
|
||||
v.literal("admitting"),
|
||||
v.literal("admitted"),
|
||||
v.literal("queued"),
|
||||
v.literal("processing"),
|
||||
v.literal("completed"),
|
||||
v.literal("failed")
|
||||
),
|
||||
submissionId: v.optional(v.string()),
|
||||
error: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
completedAt: v.optional(v.number()),
|
||||
}).index("by_conversationId_and_clientRequestId", [
|
||||
"conversationId",
|
||||
"clientRequestId",
|
||||
]),
|
||||
conversationMessages: defineTable({
|
||||
conversationId: v.id("conversations"),
|
||||
turnId: v.id("conversationTurns"),
|
||||
role: v.union(v.literal("user"), v.literal("assistant")),
|
||||
content: v.string(),
|
||||
ordinal: v.number(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_organization_and_message", [
|
||||
"organizationId",
|
||||
"conversationId",
|
||||
"messageId",
|
||||
])
|
||||
.index("by_organization_and_request", [
|
||||
"organizationId",
|
||||
"clientRequestId",
|
||||
]),
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Product Signals. A Signal is the agent-produced interpretation of one
|
||||
// or more exact user messages into a structured problem statement. It is
|
||||
// organization-scoped, optionally project-scoped, and idempotent by
|
||||
// organization + ordered source-message selection (`sourceKey`). Raw text
|
||||
// is never accepted from the agent: it is copied server-side from the
|
||||
// normalized conversation messages into immutable source snapshots.
|
||||
// -----------------------------------------------------------------
|
||||
.index("by_conversationId_and_ordinal", ["conversationId", "ordinal"])
|
||||
.index("by_turnId_and_role", ["turnId", "role"]),
|
||||
conversationAttachments: defineTable({
|
||||
messageId: v.id("conversationMessages"),
|
||||
storageId: v.id("_storage"),
|
||||
filename: v.optional(v.string()),
|
||||
mimeType: v.string(),
|
||||
createdAt: v.number(),
|
||||
}).index("by_messageId", ["messageId"]),
|
||||
signals: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.optional(v.id("projects")),
|
||||
conversationId: v.string(),
|
||||
projectId: v.id("projects"),
|
||||
conversationId: v.id("conversations"),
|
||||
sourceKey: v.string(),
|
||||
problemStatement: v.object({
|
||||
title: v.string(),
|
||||
summary: v.string(),
|
||||
desiredOutcome: v.string(),
|
||||
constraints: v.array(v.string()),
|
||||
}),
|
||||
title: v.string(),
|
||||
summary: v.string(),
|
||||
desiredOutcome: v.string(),
|
||||
processedByAgentName: v.string(),
|
||||
processedByAgentInstanceId: v.string(),
|
||||
createdAt: v.number(),
|
||||
@@ -262,26 +104,20 @@ export default defineSchema({
|
||||
.index("by_organization_and_createdAt", ["organizationId", "createdAt"])
|
||||
.index("by_organization_and_sourceKey", ["organizationId", "sourceKey"])
|
||||
.index("by_project_and_createdAt", ["projectId", "createdAt"]),
|
||||
|
||||
// Immutable ordered source snapshots copied from `conversationMessages`.
|
||||
// `ordinal` is the zero-based position in the agent's ordered selection.
|
||||
signalConstraints: defineTable({
|
||||
signalId: v.id("signals"),
|
||||
ordinal: v.number(),
|
||||
value: v.string(),
|
||||
}).index("by_signalId_and_ordinal", ["signalId", "ordinal"]),
|
||||
signalSources: defineTable({
|
||||
signalId: v.id("signals"),
|
||||
organizationId: v.id("organizations"),
|
||||
conversationId: v.string(),
|
||||
messageId: v.string(),
|
||||
messageId: v.id("conversationMessages"),
|
||||
ordinal: v.number(),
|
||||
rawTextSnapshot: v.string(),
|
||||
sourceCreatedAt: v.number(),
|
||||
submissionId: v.optional(v.string()),
|
||||
})
|
||||
.index("by_signal_and_ordinal", ["signalId", "ordinal"])
|
||||
.index("by_organization_and_message", [
|
||||
"organizationId",
|
||||
"conversationId",
|
||||
"messageId",
|
||||
]),
|
||||
|
||||
.index("by_signalId_and_ordinal", ["signalId", "ordinal"])
|
||||
.index("by_messageId", ["messageId"]),
|
||||
works: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
@@ -295,8 +131,6 @@ export default defineSchema({
|
||||
.index("by_organization_and_createdAt", ["organizationId", "createdAt"]),
|
||||
|
||||
signalWorkAttachments: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
signalId: v.id("signals"),
|
||||
workId: v.id("works"),
|
||||
createdAt: v.number(),
|
||||
@@ -306,34 +140,15 @@ export default defineSchema({
|
||||
.index("by_signal_and_work", ["signalId", "workId"]),
|
||||
|
||||
workEvents: defineTable({
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
workId: v.id("works"),
|
||||
signalId: v.id("signals"),
|
||||
kind: v.union(v.literal("work.proposed"), v.literal("signal.attached")),
|
||||
idempotencyKey: v.string(),
|
||||
data: v.any(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_work_and_createdAt", ["workId", "createdAt"])
|
||||
.index("by_work_and_idempotencyKey", ["workId", "idempotencyKey"]),
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Signal-to-issue attachments. A proper relation that links one Signal
|
||||
// to one ProjectIssue, idempotent by (signalId, issueId). Multiple
|
||||
// Signals may attach to the same ProjectIssue. The relation is scoped
|
||||
// by organization and project to prevent cross-tenant leakage.
|
||||
// -----------------------------------------------------------------
|
||||
signalIssueAttachments: defineTable({
|
||||
signalId: v.id("signals"),
|
||||
issueId: v.id("projectIssues"),
|
||||
organizationId: v.id("organizations"),
|
||||
projectId: v.id("projects"),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_signal", ["signalId"])
|
||||
.index("by_issue", ["issueId"])
|
||||
.index("by_signal_and_issue", ["signalId", "issueId"]),
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Flue persistence stores (schema/format version 4).
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user