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:
-Puter
2026-07-27 21:32:17 +05:30
parent 4c741fbe06
commit a5414d83bd
127 changed files with 1283 additions and 17784 deletions

View File

@@ -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).
// -----------------------------------------------------------------