|
| 1 | +import { |
| 2 | + boolean, |
| 3 | + index, |
| 4 | + integer, |
| 5 | + jsonb, |
| 6 | + numeric, |
| 7 | + pgTable, |
| 8 | + text, |
| 9 | + timestamp, |
| 10 | + uniqueIndex, |
| 11 | + uuid, |
| 12 | +} from "drizzle-orm/pg-core"; |
| 13 | +import { nodeStatusEnum, nodeTierEnum } from "./enums.js"; |
| 14 | +import { users } from "./users.js"; |
| 15 | + |
| 16 | +// ─── Nodes ──────────────────────────────────────────────────────────────────── |
| 17 | +// Registered compute nodes in the Maschina network. Every node runs the |
| 18 | +// services/runtime software and receives work from the daemon's EXECUTE phase. |
| 19 | +// The daemon currently routes to one internal runtime — this table is the |
| 20 | +// foundation for routing to any registered node. |
| 21 | + |
| 22 | +export const nodes = pgTable( |
| 23 | + "nodes", |
| 24 | + { |
| 25 | + id: uuid("id").primaryKey().defaultRandom(), |
| 26 | + |
| 27 | + // Owner — the user or org that registered and operates this node |
| 28 | + userId: uuid("user_id") |
| 29 | + .notNull() |
| 30 | + .references(() => users.id, { onDelete: "cascade" }), |
| 31 | + orgId: uuid("org_id"), |
| 32 | + |
| 33 | + name: text("name").notNull(), |
| 34 | + description: text("description"), |
| 35 | + |
| 36 | + status: nodeStatusEnum("status").notNull().default("pending"), |
| 37 | + tier: nodeTierEnum("tier").notNull().default("standard"), |
| 38 | + |
| 39 | + // Software version running on this node (semver) |
| 40 | + version: text("version"), |
| 41 | + |
| 42 | + // Geographic region for latency-aware routing |
| 43 | + // e.g. "us-east", "eu-west", "ap-southeast" |
| 44 | + region: text("region"), |
| 45 | + |
| 46 | + // Internal URL the daemon routes to for this node (e.g. http://1.2.3.4:8001) |
| 47 | + // Null for Maschina-operated nodes (resolved via internal DNS) |
| 48 | + internalUrl: text("internal_url"), |
| 49 | + |
| 50 | + // Economic security — staked USDC as collateral against misbehaviour |
| 51 | + // Slashing reduces this. Zero stake = micro/edge tier only. |
| 52 | + stakedUsdc: numeric("staked_usdc", { precision: 18, scale: 6 }).notNull().default("0"), |
| 53 | + |
| 54 | + // Rolling reputation score (0–100). Updated by daemon ANALYZE phase. |
| 55 | + reputationScore: numeric("reputation_score", { precision: 5, scale: 2 }) |
| 56 | + .notNull() |
| 57 | + .default("50"), |
| 58 | + |
| 59 | + // Lifetime counters — used for reputation calculation |
| 60 | + totalTasksCompleted: integer("total_tasks_completed").notNull().default(0), |
| 61 | + totalTasksFailed: integer("total_tasks_failed").notNull().default(0), |
| 62 | + totalTasksTimedOut: integer("total_tasks_timed_out").notNull().default(0), |
| 63 | + |
| 64 | + // Last time this node sent a heartbeat |
| 65 | + lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }), |
| 66 | + |
| 67 | + // TEE attestation — set when a verified-tier node submits attestation proof |
| 68 | + teeAttested: boolean("tee_attested").notNull().default(false), |
| 69 | + teeAttestedAt: timestamp("tee_attested_at", { withTimezone: true }), |
| 70 | + teeProvider: text("tee_provider"), // "amd_sev" | "intel_sgx" |
| 71 | + |
| 72 | + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), |
| 73 | + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), |
| 74 | + suspendedAt: timestamp("suspended_at", { withTimezone: true }), |
| 75 | + bannedAt: timestamp("banned_at", { withTimezone: true }), |
| 76 | + }, |
| 77 | + (t) => ({ |
| 78 | + userIdIdx: index("nodes_user_id_idx").on(t.userId), |
| 79 | + statusIdx: index("nodes_status_idx").on(t.status), |
| 80 | + tierIdx: index("nodes_tier_idx").on(t.tier), |
| 81 | + regionIdx: index("nodes_region_idx").on(t.region), |
| 82 | + // Daemon queries active nodes by tier + region for routing decisions |
| 83 | + routingIdx: index("nodes_routing_idx").on(t.status, t.tier, t.region), |
| 84 | + }), |
| 85 | +); |
| 86 | + |
| 87 | +// ─── Node Capabilities ──────────────────────────────────────────────────────── |
| 88 | +// Hardware and software capabilities advertised by each node. |
| 89 | +// Updated on node registration and whenever the node reports a change. |
| 90 | +// The daemon uses this to match tasks with capable nodes. |
| 91 | + |
| 92 | +export const nodeCapabilities = pgTable( |
| 93 | + "node_capabilities", |
| 94 | + { |
| 95 | + id: uuid("id").primaryKey().defaultRandom(), |
| 96 | + nodeId: uuid("node_id") |
| 97 | + .notNull() |
| 98 | + .unique() |
| 99 | + .references(() => nodes.id, { onDelete: "cascade" }), |
| 100 | + |
| 101 | + // CPU |
| 102 | + cpuCores: integer("cpu_cores"), |
| 103 | + cpuModel: text("cpu_model"), // e.g. "Apple M4 Pro", "AMD EPYC 9654" |
| 104 | + architecture: text("architecture"), // "amd64" | "arm64" |
| 105 | + |
| 106 | + // Memory + Storage |
| 107 | + ramGb: numeric("ram_gb", { precision: 8, scale: 2 }), |
| 108 | + storageGb: numeric("storage_gb", { precision: 10, scale: 2 }), |
| 109 | + |
| 110 | + // GPU — null if no GPU present |
| 111 | + hasGpu: boolean("has_gpu").notNull().default(false), |
| 112 | + gpuModel: text("gpu_model"), // e.g. "NVIDIA H100", "Apple M4 Pro GPU" |
| 113 | + gpuVramGb: numeric("gpu_vram_gb", { precision: 8, scale: 2 }), |
| 114 | + gpuCount: integer("gpu_count"), |
| 115 | + |
| 116 | + // OS |
| 117 | + osType: text("os_type"), // "linux" | "macos" | "windows" |
| 118 | + osVersion: text("os_version"), |
| 119 | + |
| 120 | + // Concurrency — how many tasks this node can run simultaneously |
| 121 | + maxConcurrentTasks: integer("max_concurrent_tasks").notNull().default(1), |
| 122 | + |
| 123 | + // Network |
| 124 | + networkBandwidthMbps: integer("network_bandwidth_mbps"), |
| 125 | + |
| 126 | + // Model support — array of model IDs this node can serve |
| 127 | + // e.g. ["ollama/llama3.2", "claude-haiku-4-5"] |
| 128 | + // Anthropic/OpenAI models are available to all nodes with valid API keys. |
| 129 | + // Ollama models depend on what's pulled locally. |
| 130 | + supportedModels: jsonb("supported_models").notNull().default([]), |
| 131 | + |
| 132 | + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), |
| 133 | + }, |
| 134 | + (t) => ({ |
| 135 | + nodeIdIdx: uniqueIndex("node_capabilities_node_id_idx").on(t.nodeId), |
| 136 | + hasGpuIdx: index("node_capabilities_has_gpu_idx").on(t.hasGpu), |
| 137 | + }), |
| 138 | +); |
| 139 | + |
| 140 | +// ─── Node Heartbeats ───────────────────────────────────────────────────────── |
| 141 | +// Rolling health log. Nodes ping every N seconds. The daemon marks a node |
| 142 | +// offline if no heartbeat is received within the timeout window. |
| 143 | +// Kept for short-term trend analysis — old rows are pruned by retention policy. |
| 144 | + |
| 145 | +export const nodeHeartbeats = pgTable( |
| 146 | + "node_heartbeats", |
| 147 | + { |
| 148 | + id: uuid("id").primaryKey().defaultRandom(), |
| 149 | + nodeId: uuid("node_id") |
| 150 | + .notNull() |
| 151 | + .references(() => nodes.id, { onDelete: "cascade" }), |
| 152 | + |
| 153 | + // Snapshot of resource utilisation at heartbeat time |
| 154 | + cpuUsagePct: numeric("cpu_usage_pct", { precision: 5, scale: 2 }), |
| 155 | + ramUsagePct: numeric("ram_usage_pct", { precision: 5, scale: 2 }), |
| 156 | + activeTaskCount: integer("active_task_count").notNull().default(0), |
| 157 | + |
| 158 | + // Derived health signal — set by the heartbeat handler |
| 159 | + // "online" = healthy, "degraded" = high load or partial failure, "offline" = unreachable |
| 160 | + healthStatus: text("health_status").notNull().default("online"), |
| 161 | + |
| 162 | + recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(), |
| 163 | + }, |
| 164 | + (t) => ({ |
| 165 | + nodeIdIdx: index("node_heartbeats_node_id_idx").on(t.nodeId), |
| 166 | + // Most recent heartbeat per node is the common query |
| 167 | + nodeRecordedIdx: index("node_heartbeats_node_recorded_idx").on(t.nodeId, t.recordedAt), |
| 168 | + }), |
| 169 | +); |
| 170 | + |
| 171 | +export type Node = typeof nodes.$inferSelect; |
| 172 | +export type NewNode = typeof nodes.$inferInsert; |
| 173 | +export type NodeCapabilities = typeof nodeCapabilities.$inferSelect; |
| 174 | +export type NewNodeCapabilities = typeof nodeCapabilities.$inferInsert; |
| 175 | +export type NodeHeartbeat = typeof nodeHeartbeats.$inferSelect; |
| 176 | +export type NewNodeHeartbeat = typeof nodeHeartbeats.$inferInsert; |
0 commit comments