{"repo": "castlemilk/omega", "files": {"docs/superpowers/plans/2026-03-21-omega-web-dashboard.md": {"f998ae4d4411dd7cc4dd08c7d132ed0967a35990": {"t": 1785746457.3078933, "n": 100004, "h": "0bdffc42d150ad01", "c": "# Omega Web Dashboard — Go + Connect-RPC + Protobuf Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Build a Go Connect-RPC API server + React/TypeScript dashboard with Connect-ES that reads from Omega's SQLite state store and renders live visibility into the self-improving agent system.\n\n**Architecture:** Proto definitions are the contract. Go server (`cmd/omega-api`) reads from the shared SQLite DBs that the Python node layer writes to. React dashboard uses `@connectrpc/connect-web` to call the Go server directly — no REST, no WebSockets. Real-time updates use server-streaming RPC (`StreamEvents`).\n\n**Tech Stack:** Go 1.24+, `connectrpc.com/connect`, `modernc.org/sqlite`, `google.golang.org/protobuf`, Buf CLI; Vite 5, React 18, TypeScript, `@connectrpc/connect-web` v2, `@bufbuild/protobuf`, Tailwind CSS, shadcn/ui, recharts\n\n---\n\n## Key Data Facts (from existing Python codebase)\n\n- **State store DB**: `/tmp/omega_victoria_state.db`\n - Tables: `nodes`, `node_executions`, `traces`, `cost_events`, `issues`, `activity_log`, `improvement_log`, `config_revisions`\n- **Memory DB**: `/tmp/omega_victoria_memory.db`\n - Tables: `episodes`, `semantic_memories`, `memory_ratings`\n- **Convergence data**: `episodes WHERE event_type='cycle_summary'` contains `score`, `pipeline_ms` per cycle\n- **Node namespacing**: memory is namespaced by node name (e.g., `\"DataIngestionNode\"`)\n- **Toolchain available**: Go 1.26, Buf 1.47, Node 22, npm 10\n\n---\n\n## File Map\n\n### Proto definitions\n| File | Responsibility |\n|------|---------------|\n| `buf.yaml` | Buf workspace config |\n| `buf.gen.yaml` | Code generation config (Go + TypeScript) |\n| `proto/omega/v1/omega_service.proto` | OrchestratorService — all dashboard RPCs |\n| `proto/omega/v1/node_service.proto` | NodeService — inter-node communication contract |\n| `proto/omega/v1/types.proto` | Shared message types (Node, Span, Issue, etc.) |\n\n### Go backend\n| File | Responsibility |\n|------|---------------|\n| `go.mod` | Go module definition |\n| `cmd/omega-api/main.go` | Server entry point, Connect handler registration |\n| `internal/db/db.go` | SQLite read layer — all queries |\n| `internal/handler/orchestrator.go` | OrchestratorService implementation |\n\n### Generated (do not hand-edit)\n| Path | Generated by |\n|------|-------------|\n| `gen/go/omega/v1/` | buf generate → Go protobuf + connect stubs |\n| `dashboard/src/gen/` | buf generate → TypeScript protobuf + connect clients |\n\n### Frontend\n| File | Responsibility |\n|------|---------------|\n| `dashboard/src/client.ts` | Connect transport + service client factory |\n| `dashboard/src/components/layout/Sidebar.tsx` | Nav sidebar |\n| `dashboard/src/components/layout/Header.tsx` | Status bar + orchestrator controls |\n| `dashboard/src/components/NodeCard.tsx` | Node health card with sparkline |\n| `dashboard/src/components/TraceWaterfall.tsx` | SVG waterfall for span trees |\n| `dashboard/src/components/BrainConfigPanel.tsx` | LLM model switcher panel (provider + model selectors) |\n| `dashboard/src/pages/Dashboard.tsx` | Overview page |\n| `dashboard/src/pages/Nodes.tsx` | Node table + detail + BrainConfigPanel |\n| `dashboard/src/pages/Traces.tsx` | Trace list + waterfall |\n| `dashboard/src/pages/Metrics.tsx` | Time-series charts |\n| `dashboard/src/pages/Issues.tsx` | Issues table |\n| `dashboard/src/pages/Memory.tsx` | Memory kernel visualization |\n| `dashboard/src/pages/Convergence.tsx` | Convergence + improvement log |\n| `dashboard/src/App.tsx` | Router + layout wrapper |\n\n---\n\n## Task 1: Proto Definitions\n\n**Files:**\n- Create: `buf.yaml`\n- Create: `buf.gen.yaml`\n- Create: `proto/omega/v1/types.proto`\n- Create: `proto/omega/v1/omega_service.proto`\n- Create: `proto/omega/v1/node_service.proto`\n\n- [ ] **Step 1: Write `buf.yaml`**\n\n```yaml\nversion: v2\nmodules:\n - path: proto\ndeps:\n - buf.build/googleapis/googleapis\nlint:\n use:\n - STANDARD\nbreaking:\n use:\n - FILE\n```\n\n- [ ] **Step 2: Write `buf.gen.yaml`**\n\n```yaml\nversion: v2\nmanaged:\n enabled: true\nplugins:\n - remote: buf.build/protocolbuffers/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/connectrpc/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/bufbuild/es\n out: dashboard/src/gen\n opt:\n - target=ts\n - remote: buf.build/connectrpc/es\n out: dashboard/src/gen\n opt:\n - target=ts\n```\n\n- [ ] **Step 3: Write `proto/omega/v1/types.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage Node {\n string node_id = 1;\n string name = 2;\n string version = 3;\n repeated string capabilities = 4;\n double health = 5;\n string status = 6;\n google.protobuf.Timestamp registered_at = 7;\n google.protobuf.Timestamp last_updated = 8;\n int64 executions_total = 9;\n double error_rate = 10;\n double avg_latency_ms = 11;\n double p95_latency_ms = 12;\n int64 improvement_count = 13;\n ExecutionRecord last_execution = 14;\n}\n\nmessage ExecutionRecord {\n string exec_id = 1;\n string node_id = 2;\n string node_name = 3;\n string trace_id = 4;\n string span_id = 5;\n string action = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n bool success = 10;\n string error_text = 11;\n map metrics = 12;\n int64 cycle = 13;\n}\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage TraceSummary {\n string trace_id = 1;\n google.protobuf.Timestamp trace_started = 2;\n google.protobuf.Timestamp trace_ended = 3;\n double total_duration_ms = 4;\n int64 span_count = 5;\n int64 error_spans = 6;\n int64 cycle = 7;\n}\n\nmessage Span {\n string span_id = 1;\n string trace_id = 2;\n string parent_span_id = 3;\n string node_id = 4;\n string node_name = 5;\n string operation = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n string status = 10;\n int64 cycle = 11;\n}\n\n// ── Issues ─────────────────────────────────────────────────────────────────\n\nmessage Issue {\n string issue_id = 1;\n string detector = 2;\n string severity = 3;\n string description = 4;\n string state = 5;\n google.protobuf.Timestamp opened_at = 6;\n google.protobuf.Timestamp resolved_at = 7;\n int64 cycle_opened = 8;\n int64 cycle_resolved = 9;\n}\n\n// ── Activity ───────────────────────────────────────────────────────────────\n\nmessage ActivityEntry {\n string log_id = 1;\n string action_type = 2;\n string entity_type = 3;\n string entity_id = 4;\n google.protobuf.Timestamp recorded_at = 5;\n int64 cycle = 6;\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────\n\nmessage ImprovementRecord {\n string improve_id = 1;\n string node_id = 2;\n string node_name = 3;\n string from_version = 4;\n string to_version = 5;\n string triggered_by = 6;\n google.protobuf.Timestamp recorded_at = 7;\n int64 cycle = 8;\n map before_metrics = 9;\n map after_metrics = 10;\n}\n\n// ── System health ──────────────────────────────────────────────────────────\n\nmessage SystemHealth {\n string status = 1;\n double composite_score = 2;\n double avg_node_health = 3;\n int64 node_count = 4;\n int64 open_issues = 5;\n int64 error_issues = 6;\n double uptime_seconds = 7;\n int64 total_cycles = 8;\n}\n\n// ── Memory ─────────────────────────────────────────────────────────────────\n\nmessage SemanticConcept {\n string concept = 1;\n string content = 2;\n double confidence = 3;\n int64 evidence_count = 4;\n repeated string tags = 5;\n}\n\nmessage EpisodeEntry {\n string episode_id = 1;\n string event_type = 2;\n google.protobuf.Timestamp timestamp = 3;\n int64 cycle = 4;\n double importance = 5;\n repeated string tags = 6;\n}\n\n// ── Convergence ────────────────────────────────────────────────────────────\n\nmessage ConvergencePoint {\n int64 cycle = 1;\n google.protobuf.Timestamp timestamp = 2;\n double score = 3;\n double pipeline_ms = 4;\n}\n\n// ── Costs ──────────────────────────────────────────────────────────────────\n\nmessage CostEntry {\n string provider = 1;\n string node_id = 2;\n int64 calls = 3;\n double total_ms = 4;\n double total_cost_usd = 5;\n}\n\n// ── Events (for streaming) ─────────────────────────────────────────────────\n\nmessage EventResponse {\n string type = 1;\n oneof payload {\n SystemHealth health_update = 2;\n Node node_update = 3;\n Issue new_issue = 4;\n ImprovementRecord improvement = 5;\n ActivityEntry activity = 6;\n }\n}\n\n// ── LLM Brain config ───────────────────────────────────────────────────────\n\nmessage BrainConfig {\n string provider = 1; // \"anthropic\", \"openai\", \"google\", \"deepseek\", \"ollama\", \"codex\"\n string model = 2; // e.g. \"claude-sonnet-4-6\", \"gpt-4o\", \"gemini-2.0-flash\"\n double temperature = 3;\n int32 max_tokens = 4;\n string system_prompt = 5;\n map extra_config = 6;\n}\n\nmessage NodeConfig {\n string node_id = 1;\n string node_type = 2;\n BrainConfig brain = 3;\n map parameters = 4;\n string version = 5;\n google.protobuf.Timestamp updated_at = 6;\n}\n\nmessage BrainExecutionEntry {\n string exec_id = 1;\n string node_id = 2;\n string provider = 3;\n string model = 4;\n int64 prompt_tokens = 5;\n int64 completion_tokens = 6;\n double latency_ms = 7;\n bool success = 8;\n string error_text = 9;\n google.protobuf.Timestamp executed_at = 10;\n int64 cycle = 11;\n}\n\nmessage ProviderInfo {\n string id = 1; // \"anthropic\"\n string display_name = 2; // \"Anthropic\"\n repeated string models = 3;\n bool requires_api_key = 4;\n bool available = 5;\n}\n```\n\n- [ ] **Step 4: Write `proto/omega/v1/omega_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"omega/v1/types.proto\";\n\nservice OrchestratorService {\n rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);\n rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);\n rpc GetNode(GetNodeRequest) returns (GetNodeResponse);\n rpc ListNodeExecutions(ListNodeExecutionsRequest) returns (ListNodeExecutionsResponse);\n rpc ListTraces(ListTracesRequest) returns (ListTracesResponse);\n rpc GetTrace(GetTraceRequest) returns (GetTraceResponse);\n rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);\n rpc GetMetricsHistory(GetMetricsHistoryRequest) returns (GetMetricsHistoryResponse);\n rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);\n rpc ListActivity(ListActivityRequest) returns (ListActivityResponse);\n rpc ListImprovements(ListImprovementsRequest) returns (ListImprovementsResponse);\n rpc GetNodeMemory(GetNodeMemoryRequest) returns (GetNodeMemoryResponse);\n rpc GetConvergence(GetConvergenceRequest) returns (GetConvergenceResponse);\n rpc GetCosts(GetCostsRequest) returns (GetCostsResponse);\n rpc SubmitFeedback(SubmitFeedbackRequest) returns (SubmitFeedbackResponse);\n rpc StartOrchestrator(StartOrchestratorRequest) returns (StartOrchestratorResponse);\n rpc StopOrchestrator(StopOrchestratorRequest) returns (StopOrchestratorResponse);\n rpc TriggerHeartbeat(TriggerHeartbeatRequest) returns (TriggerHeartbeatResponse);\n rpc StreamEvents(StreamEventsRequest) returns (stream EventResponse);\n\n // ── Brain / LLM config ───────────────────────────────────────────────────\n rpc GetNodeConfig(GetNodeConfigRequest) returns (GetNodeConfigResponse);\n rpc UpdateNodeBrain(UpdateNodeBrainRequest) returns (UpdateNodeBrainResponse);\n rpc ListAvailableProviders(ListAvailableProvidersRequest) returns (ListAvailableProvidersResponse);\n rpc GetBrainExecutionHistory(GetBrainExecutionHistoryRequest) returns (GetBrainExecutionHistoryResponse);\n}\n\n// ── Health ─────────────────────────────────────────────────────────────────\n\nmessage GetHealthRequest {}\nmessage GetHealthResponse { SystemHealth health = 1; }\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage ListNodesRequest {}\nmessage ListNodesResponse { repeated Node nodes = 1; }\n\nmessage GetNodeRequest { string node_id = 1; }\nmessage GetNodeResponse {\n Node node = 1;\n repeated ExecutionRecord recent_executions = 2;\n repeated ImprovementRecord improvements = 3;\n repeated LatencyPoint latency_history = 4;\n}\n\nmessage LatencyPoint {\n google.protobuf.Timestamp ts = 1;\n double duration_ms = 2;\n bool success = 3;\n}\n\nmessage ListNodeExecutionsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListNodeExecutionsResponse { repeated ExecutionRecord executions = 1; }\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage ListTracesRequest { int32 limit = 1; }\nmessage ListTracesResponse { repeated TraceSummary traces = 1; }\n\nmessage GetTraceRequest { string trace_id = 1; }\nmessage GetTraceResponse { repeated Span spans = 1; }\n\n// ── Metrics ─────────────────────────────────────────────────────────────────\n\nmessage GetMetricsRequest {}\nmessage GetMetricsResponse {\n SystemHealth system = 1;\n repeated Node nodes = 2;\n repeated CostEntry costs = 3;\n repeated Issue open_issues = 4;\n repeated TraceSummary recent_traces = 5;\n}\n\nmessage GetMetricsHistoryRequest { string node_id = 1; }\nmessage GetMetricsHistoryResponse {\n repeated NodeLatencyHistory node_histories = 1;\n}\nmessage NodeLatencyHistory {\n string node_id = 1;\n string name = 2;\n repeated LatencyPoint history = 3;\n}\n\n// ── Issues ──────────────────────────────────────────────────────────────────\n\nmessage ListIssuesRequest { string state_filter = 1; }\nmessage ListIssuesResponse { repeated Issue issues = 1; }\n\n// ── Activity ─────────────────────────────────────────────────────────────────\n\nmessage ListActivityRequest { int32 limit = 1; }\nmessage ListActivityResponse { repeated ActivityEntry entries = 1; }\n\n// ── Improvements ─────────────────────────────────────────────────────────────\n\nmessage ListImprovementsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListImprovementsResponse { repeated ImprovementRecord improvements = 1; }\n\n// ── Memory ───────────────────────────────────────────────────────────────────\n\nmessage GetNodeMemoryRequest { string node_id = 1; }\nmessage GetNodeMemoryResponse {\n string namespace = 1;\n int64 episodic_count = 2;\n int64 semantic_count = 3;\n repeated SemanticConcept top_semantic = 4;\n repeated EpisodeEntry recent_episodes = 5;\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nmessage GetConvergenceRequest { int32 limit = 1; }\nmessage GetConvergenceResponse { repeated ConvergencePoint points = 1; }\n\n// ── Costs ────────────────────────────────────────────────────────────────────\n\nmessage GetCostsRequest {}\nmessage GetCostsResponse { repeated CostEntry costs = 1; }\n\n// ── Feedback ─────────────────────────────────────────────────────────────────\n\nmessage SubmitFeedbackRequest {\n string text = 1;\n string target_node = 2;\n}\nmessage SubmitFeedbackResponse { bool ok = 1; }\n\n// ── Orchestrator control ─────────────────────────────────────────────────────\n\nmessage StartOrchestratorRequest { int32 heartbeat_secs = 1; }\nmessage StartOrchestratorResponse { bool started = 1; string message = 2; }\n\nmessage StopOrchestratorRequest {}\nmessage StopOrchestratorResponse { bool stopped = 1; string message = 2; }\n\nmessage TriggerHeartbeatRequest {}\nmessage TriggerHeartbeatResponse { bool triggered = 1; string message = 2; }\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nmessage StreamEventsRequest { int32 poll_interval_ms = 1; }\n\n// ── Brain config ──────────────────────────────────────────────────────────────\n\nmessage GetNodeConfigRequest { string node_id = 1; }\nmessage GetNodeConfigResponse { NodeConfig config = 1; }\n\nmessage UpdateNodeBrainRequest {\n string node_id = 1;\n BrainConfig brain = 2;\n}\nmessage UpdateNodeBrainResponse { NodeConfig config = 1; }\n\nmessage ListAvailableProvidersRequest {}\nmessage ListAvailableProvidersResponse { repeated ProviderInfo providers = 1; }\n\nmessage GetBrainExecutionHistoryRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage GetBrainExecutionHistoryResponse { repeated BrainExecutionEntry entries = 1; }\n```\n\n- [ ] **Step 5: Write `proto/omega/v1/node_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\n// NodeService — contract for inter-node communication.\n// Python nodes implement this; Go orchestrator calls it.\nservice NodeService {\n rpc Execute(ExecuteRequest) returns (ExecuteResponse);\n rpc Evaluate(EvaluateRequest) returns (EvaluateResponse);\n rpc Improve(ImproveRequest) returns (ImproveResponse);\n rpc GetState(GetStateRequest) returns (GetStateResponse);\n rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);\n}\n\nmessage ExecuteRequest {\n string action = 1;\n map parameters = 2;\n map context = 3;\n}\n\nmessage ExecuteResponse {\n bool success = 1;\n bytes result_json = 2;\n repeated string errors = 3;\n map metrics = 4;\n}\n\nmessage EvaluateRequest {}\nmessage EvaluateResponse { map metrics = 1; }\n\nmessage ImproveRequest { map feedback = 1; }\nmessage ImproveResponse { bool changed = 1; string new_version = 2; }\n\nmessage GetStateRequest {}\nmessage GetStateResponse {\n string node_id = 1;\n string name = 2;\n string version = 3;\n double health = 4;\n string status = 5;\n repeated string capabilities = 6;\n}\n\nmessage GetCapabilitiesRequest {}\nmessage GetCapabilitiesResponse { repeated string capabilities = 1; }\n```\n\n- [ ] **Step 6: Lint and validate protos**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf dep update\nbuf lint\n```\n\nExpected: no errors.\n\n- [ ] **Step 7: Commit protos and buf config**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add buf.yaml buf.gen.yaml proto/\ngit commit -m \"feat(proto): add Omega protobuf service definitions\"\n```\n\n---\n\n## Task 2: Go Module + Code Generation\n\n**Files:**\n- Create: `go.mod`\n- Create: `go.sum` (generated)\n- Create: `gen/go/` (generated)\n- Create: `dashboard/src/gen/` (generated)\n\n- [ ] **Step 1: Initialize Go module**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo mod init github.com/benebsworth/omega\n```\n\n- [ ] **Step 2: Add Go dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo get connectrpc.com/connect@latest\ngo get google.golang.org/protobuf@latest\ngo get modernc.org/sqlite@latest\ngo get golang.org/x/net@latest\n```\n\n- [ ] **Step 3: Run buf generate**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf generate\n```\n\nExpected: creates `gen/go/omega/v1/*.pb.go` and `gen/go/omega/v1/*connect*.go`, and `dashboard/src/gen/` with TypeScript files.\n\n- [ ] **Step 4: Verify generated Go compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./gen/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 5: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add go.mod go.sum gen/ dashboard/src/gen/\ngit commit -m \"feat(gen): add Go module and run buf generate for Go + TS stubs\"\n```\n\n---\n\n## Task 3: Go Database Layer\n\n**Files:**\n- Create: `internal/db/db.go`\n\n- [ ] **Step 1: Write `internal/db/db.go`**\n\n```go\n// Package db provides read-only access to the Omega SQLite state and memory databases.\n// The Python node layer writes; this Go layer reads.\npackage db\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t_ \"modernc.org/sqlite\"\n)\n\nconst (\n\tStateDBPath = \"/tmp/omega_victoria_state.db\"\n\tMemoryDBPath = \"/tmp/omega_victoria_memory.db\"\n)\n\n// DB holds connections to both Omega SQLite databases.\ntype DB struct {\n\tstate *sql.DB\n\tmemory *sql.DB\n}\n\n// New opens both databases in WAL mode for concurrent reads.\nfunc New(stateDBPath, memoryDBPath string) (*DB, error) {\n\tstate, err := openDB(stateDBPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open state db: %w\", err)\n\t}\n\tmemory, err := openDB(memoryDBPath)\n\tif err != nil {\n\t\tstate.Close()\n\t\treturn nil, fmt.Errorf(\"open memory db: %w\", err)\n\t}\n\td := &DB{state: state, memory: memory}\n\tif err := d.ensureBrainTables(); err != nil {\n\t\tstate.Close()\n\t\tmemory.Close()\n\t\treturn nil, fmt.Errorf(\"ensure brain tables: %w\", err)\n\t}\n\treturn d, nil\n}\n\nfunc openDB(path string) (*sql.DB, error) {\n\tdb, err := sql.Open(\"sqlite\", path+\"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdb.SetMaxOpenConns(1) // SQLite WAL: one writer, many readers via connection pooling\n\tdb.SetMaxIdleConns(1)\n\treturn db, db.Ping()\n}\n\nfunc (d *DB) Close() {\n\td.state.Close()\n\td.memory.Close()\n}\n\n// ── Node types ──────────────────────────────────────────────────────────────\n\ntype Node struct {\n\tNodeID string\n\tName string\n\tVersion string\n\tCapabilities []string\n\tHealth float64\n\tStatus string\n\tRegisteredAt float64\n\tLastUpdated float64\n\tExecutionsTotal int64\n\tErrorRate float64\n\tAvgLatencyMS float64\n\tP95LatencyMS float64\n\tImprovementCount int64\n\tLastExecution *Execution\n}\n\ntype Execution struct {\n\tExecID string\n\tNodeID string\n\tNodeName string\n\tTraceID string\n\tSpanID string\n\tAction string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tSuccess bool\n\tErrorText string\n\tMetrics map[string]float64\n\tCycle int64\n}\n\ntype LatencyPoint struct {\n\tStartedAt float64\n\tDurationMS float64\n\tSuccess bool\n}\n\n// ── Trace types ─────────────────────────────────────────────────────────────\n\ntype TraceSummary struct {\n\tTraceID string\n\tTraceStarted float64\n\tTraceEnded *float64\n\tTotalDurationMS float64\n\tSpanCount int64\n\tErrorSpans int64\n\tCycle int64\n}\n\ntype Span struct {\n\tSpanID string\n\tTraceID string\n\tParentSpanID string\n\tNodeID string\n\tNodeName string\n\tOperation string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tStatus string\n\tCycle int64\n}\n\n// ── Issue types ──────────────────────────────────────────────────────────────\n\ntype Issue struct {\n\tIssueID string\n\tDetector string\n\tSeverity string\n\tDescription string\n\tState string\n\tOpenedAt float64\n\tResolvedAt *float64\n\tCycleOpened int64\n\tCycleResolved *int64\n}\n\n// ── Activity types ────────────────────────────────────────────────────────────\n\ntype ActivityEntry struct {\n\tLogID string\n\tActionType string\n\tEntityType string\n\tEntityID string\n\tRecordedAt float64\n\tCycle int64\n}\n\n// ── Improvement types ─────────────────────────────────────────────────────────\n\ntype Improvement struct {\n\tImproveID string\n\tNodeID string\n\tNodeName string\n\tFromVersion string\n\tToVersion string\n\tTriggeredBy string\n\tRecordedAt float64\n\tCycle int64\n\tBeforeMetrics map[string]float64\n\tAfterMetrics map[string]float64\n}\n\n// ── Cost types ────────────────────────────────────────────────────────────────\n\ntype CostEntry struct {\n\tProvider string\n\tNodeID string\n\tCalls int64\n\tTotalMS float64\n\tTotalCostUSD float64\n}\n\n// ── Memory types ──────────────────────────────────────────────────────────────\n\ntype SemanticConcept struct {\n\tConcept string\n\tContent string\n\tConfidence float64\n\tEvidenceCount int64\n\tTags []string\n}\n\ntype EpisodeEntry struct {\n\tEpisodeID string\n\tEventType string\n\tTimestamp float64\n\tCycle int64\n\tImportance float64\n\tTags []string\n}\n\ntype ConvergencePoint struct {\n\tCycle int64\n\tTimestamp float64\n\tScore float64\n\tPipelineMS float64\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\ntype SystemHealth struct {\n\tStatus string\n\tCompositeScore float64\n\tAvgNodeHealth float64\n\tNodeCount int64\n\tOpenIssues int64\n\tErrorIssues int64\n\tUptimeSeconds float64\n\tTotalCycles int64\n}\n\n// ── Query methods ──────────────────────────────────────────────────────────────\n\n// AllNodes returns all registered nodes with computed metrics.\nfunc (d *DB) AllNodes() ([]*Node, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes ORDER BY registered_at`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar nodes []*Node\n\tfor rows.Next() {\n\t\tn := &Node{}\n\t\tvar capsJSON string\n\t\tif err := rows.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\t\tnodes = append(nodes, n)\n\t}\n\n\t// Enrich with computed metrics\n\tfor _, n := range nodes {\n\t\tif err := d.enrichNode(n); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn nodes, nil\n}\n\n// GetNode returns one node by ID, enriched with metrics.\nfunc (d *DB) GetNode(nodeID string) (*Node, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes WHERE node_id = ?`, nodeID)\n\tn := &Node{}\n\tvar capsJSON string\n\tif err := row.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\treturn n, d.enrichNode(n)\n}\n\nfunc (d *DB) enrichNode(n *Node) error {\n\t// Execution stats\n\trow := d.state.QueryRow(`\n\t\tSELECT COUNT(*),\n\t\t SUM(CASE WHEN success=0 THEN 1 ELSE 0 END),\n\t\t AVG(duration_ms),\n\t\t MAX(started_at)\n\t\tFROM node_executions WHERE node_id = ?`, n.NodeID)\n\tvar total, failed int64\n\tvar avgMS sql.NullFloat64\n\tvar lastStarted sql.NullFloat64\n\trow.Scan(&total, &failed, &avgMS, &lastStarted) //nolint:errcheck\n\n\tn.ExecutionsTotal = total\n\tif total > 0 {\n\t\tn.ErrorRate = float64(failed) / float64(total)\n\t}\n\tif avgMS.Valid {\n\t\tn.AvgLatencyMS = avgMS.Float64\n\t}\n\n\t// p95 latency — two-query approach (SQLite rejects COUNT(*) inside OFFSET)\n\tvar p95Count int64\n\td.state.QueryRow(`SELECT COUNT(*) FROM node_executions WHERE node_id = ? AND duration_ms IS NOT NULL`, n.NodeID).Scan(&p95Count) //nolint:errcheck\n\tif p95Count > 0 {\n\t\toffset := int64(float64(p95Count) * 0.95)\n\t\tp95row := d.state.QueryRow(`\n\t\t\tSELECT duration_ms FROM node_executions\n\t\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\t\tORDER BY duration_ms\n\t\t\tLIMIT 1 OFFSET ?\n\t\t`, n.NodeID, offset)\n\t\tvar p95 sql.NullFloat64\n\t\tp95row.Scan(&p95) //nolint:errcheck\n\t\tif p95.Valid {\n\t\t\tn.P95LatencyMS = p95.Float64\n\t\t}\n\t}\n\n\t// Improvement count\n\trow2 := d.state.QueryRow(\n\t\t`SELECT COUNT(*) FROM improvement_log WHERE node_id = ?`, n.NodeID)\n\trow2.Scan(&n.ImprovementCount) //nolint:errcheck\n\n\t// Last execution\n\tlastExec, err := d.lastExecution(n.NodeID)\n\tif err == nil {\n\t\tn.LastExecution = lastExec\n\t}\n\treturn nil\n}\n\nfunc (d *DB) lastExecution(nodeID string) (*Execution, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\t action, started_at, ended_at, duration_ms, success,\n\t\t COALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE node_id = ?\n\t\tORDER BY started_at DESC LIMIT 1`, nodeID)\n\treturn scanExecution(row)\n}\n\nfunc scanExecution(row *sql.Row) (*Execution, error) {\n\te := &Execution{}\n\tvar metricsJSON string\n\tvar success int\n\tvar endedAt, durationMS sql.NullFloat64\n\terr := row.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t&e.ErrorText, &metricsJSON, &e.Cycle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\te.Success = success == 1\n\tif endedAt.Valid {\n\t\tv := endedAt.Float64\n\t\te.EndedAt = &v\n\t}\n\tif durationMS.Valid {\n\t\tv := durationMS.Float64\n\t\te.DurationMS = &v\n\t}\n\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\treturn e, nil\n}\n\n// GetExecutions returns recent executions, optionally filtered by node.\nfunc (d *DB) GetExecutions(nodeID string, limit int) ([]*Execution, error) {\n\tquery := `SELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\taction, started_at, ended_at, duration_ms, success,\n\t\tCOALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY started_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\treturn scanExecutions(rows)\n}\n\nfunc scanExecutions(rows *sql.Rows) ([]*Execution, error) {\n\tvar result []*Execution\n\tfor rows.Next() {\n\t\te := &Execution{}\n\t\tvar metricsJSON string\n\t\tvar success int\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t\t&e.ErrorText, &metricsJSON, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\te.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\te.DurationMS = &v\n\t\t}\n\t\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\t\tresult = append(result, e)\n\t}\n\treturn result, nil\n}\n\n// LatencyHistory returns (started_at, duration_ms, success) for charting.\nfunc (d *DB) LatencyHistory(nodeID string, limit int) ([]*LatencyPoint, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT started_at, duration_ms, success\n\t\tFROM node_executions\n\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\tORDER BY started_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*LatencyPoint\n\tfor rows.Next() {\n\t\tp := &LatencyPoint{}\n\t\tvar success int\n\t\tif err := rows.Scan(&p.StartedAt, &p.DurationMS, &success); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.Success = success == 1\n\t\tpoints = append(points, p)\n\t}\n\t// Reverse to chronological order\n\tfor i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {\n\t\tpoints[i], points[j] = points[j], points[i]\n\t}\n\treturn points, nil\n}\n\n// RecentTraces returns recent trace summaries.\nfunc (d *DB) RecentTraces(limit int) ([]*TraceSummary, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT trace_id,\n\t\t MIN(started_at) as trace_started,\n\t\t MAX(ended_at) as trace_ended,\n\t\t (MAX(ended_at) - MIN(started_at)) * 1000 as total_duration_ms,\n\t\t COUNT(*) as span_count,\n\t\t SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_spans,\n\t\t MAX(cycle) as cycle\n\t\tFROM traces\n\t\tGROUP BY trace_id\n\t\tORDER BY trace_started DESC\n\t\tLIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar summaries []*TraceSummary\n\tfor rows.Next() {\n\t\tt := &TraceSummary{}\n\t\tvar traceEnded sql.NullFloat64\n\t\tif err := rows.Scan(&t.TraceID, &t.TraceStarted, &traceEnded,\n\t\t\t&t.TotalDurationMS, &t.SpanCount, &t.ErrorSpans, &t.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif traceEnded.Valid {\n\t\t\tv := traceEnded.Float64\n\t\t\tt.TraceEnded = &v\n\t\t}\n\t\tsummaries = append(summaries, t)\n\t}\n\treturn summaries, nil\n}\n\n// GetTraceSpans returns all spans for a trace.\nfunc (d *DB) GetTraceSpans(traceID string) ([]*Span, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT span_id, trace_id, COALESCE(parent_span_id,''),\n\t\t COALESCE(node_id,''), COALESCE(node_name,''),\n\t\t operation, started_at, ended_at, duration_ms, status, cycle\n\t\tFROM traces WHERE trace_id = ? ORDER BY started_at`, traceID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar spans []*Span\n\tfor rows.Next() {\n\t\ts := &Span{}\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&s.SpanID, &s.TraceID, &s.ParentSpanID,\n\t\t\t&s.NodeID, &s.NodeName, &s.Operation,\n\t\t\t&s.StartedAt, &endedAt, &durationMS, &s.Status, &s.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\ts.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\ts.DurationMS = &v\n\t\t}\n\t\tspans = append(spans, s)\n\t}\n\treturn spans, nil\n}\n\n// GetIssues returns issues, optionally filtered by state.\nfunc (d *DB) GetIssues(stateFilter string) ([]*Issue, error) {\n\tquery := `SELECT issue_id, detector, severity, description, state,\n\t\topened_at, resolved_at, cycle_opened, cycle_resolved\n\t\tFROM issues WHERE 1=1`\n\targs := []any{}\n\tif stateFilter != \"\" && stateFilter != \"all\" {\n\t\tif stateFilter == \"open\" {\n\t\t\tquery += \" AND state != 'resolved'\"\n\t\t} else {\n\t\t\tquery += \" AND state = ?\"\n\t\t\targs = append(args, stateFilter)\n\t\t}\n\t}\n\tquery += \" ORDER BY opened_at DESC LIMIT 100\"\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar issues []*Issue\n\tfor rows.Next() {\n\t\ti := &Issue{}\n\t\tif err := rows.Scan(&i.IssueID, &i.Detector, &i.Severity, &i.Description,\n\t\t\t&i.State, &i.OpenedAt, &i.ResolvedAt, &i.CycleOpened, &i.CycleResolved); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tissues = append(issues, i)\n\t}\n\treturn issues, nil\n}\n\n// RecentActivity returns recent activity log entries.\nfunc (d *DB) RecentActivity(limit int) ([]*ActivityEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT log_id, action_type, entity_type, entity_id, recorded_at, cycle\n\t\tFROM activity_log ORDER BY recorded_at DESC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*ActivityEntry\n\tfor rows.Next() {\n\t\te := &ActivityEntry{}\n\t\tif err := rows.Scan(&e.LogID, &e.ActionType, &e.EntityType,\n\t\t\t&e.EntityID, &e.RecordedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n\n// GetImprovements returns improvement log, optionally by node.\nfunc (d *DB) GetImprovements(nodeID string, limit int) ([]*Improvement, error) {\n\tquery := `SELECT improve_id, node_id, node_name, from_version, to_version,\n\t\ttriggered_by, recorded_at, cycle, before_metrics_json, after_metrics_json\n\t\tFROM improvement_log WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY recorded_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar imps []*Improvement\n\tfor rows.Next() {\n\t\timp := &Improvement{}\n\t\tvar beforeJSON, afterJSON string\n\t\tif err := rows.Scan(&imp.ImproveID, &imp.NodeID, &imp.NodeName,\n\t\t\t&imp.FromVersion, &imp.ToVersion, &imp.TriggeredBy,\n\t\t\t&imp.RecordedAt, &imp.Cycle, &beforeJSON, &afterJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(beforeJSON), &imp.BeforeMetrics) //nolint:errcheck\n\t\tjson.Unmarshal([]byte(afterJSON), &imp.AfterMetrics) //nolint:errcheck\n\t\timps = append(imps, imp)\n\t}\n\treturn imps, nil\n}\n\n// GetCosts returns aggregated cost by provider and node.\nfunc (d *DB) GetCosts() ([]*CostEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT provider, node_id,\n\t\t COUNT(*) as calls,\n\t\t SUM(duration_ms) as total_ms,\n\t\t SUM(estimated_cost_usd) as total_cost\n\t\tFROM cost_events GROUP BY provider, node_id`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar costs []*CostEntry\n\tfor rows.Next() {\n\t\tc := &CostEntry{}\n\t\tif err := rows.Scan(&c.Provider, &c.NodeID, &c.Calls, &c.TotalMS, &c.TotalCostUSD); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcosts = append(costs, c)\n\t}\n\treturn costs, nil\n}\n\n// GetConvergence reads cycle_summary episodes from the memory DB.\nfunc (d *DB) GetConvergence(limit int) ([]*ConvergencePoint, error) {\n\trows, err := d.memory.Query(`\n\t\tSELECT cycle, timestamp, content_json\n\t\tFROM episodes WHERE event_type = 'cycle_summary'\n\t\tORDER BY cycle ASC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*ConvergencePoint\n\tfor rows.Next() {\n\t\tp := &ConvergencePoint{}\n\t\tvar contentJSON string\n\t\tif err := rows.Scan(&p.Cycle, &p.Timestamp, &contentJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar content map[string]any\n\t\tif err := json.Unmarshal([]byte(contentJSON), &content); err == nil {\n\t\t\tif s, ok := content[\"score\"].(float64); ok {\n\t\t\t\tp.Score = s\n\t\t\t}\n\t\t\tif ms, ok := content[\"pipeline_ms\"].(float64); ok {\n\t\t\t\tp.PipelineMS = ms\n\t\t\t}\n\t\t}\n\t\tpoints = append(points, p)\n\t}\n\treturn points, nil\n}\n\n// GetMemoryStats returns episodic + semantic counts for a namespace.\nfunc (d *DB) GetMemoryStats(namespace string) (int64, int64, []*SemanticConcept, []*EpisodeEntry, error) {\n\tvar epCount, semCount int64\n\n\t// Episodic count\n\tquery := \"SELECT COUNT(*) FROM episodes\"\n\targs := []any{}\n\tif namespace != \"\" {\n\t\tquery += \" WHERE namespace = ?\"\n\t\targs = append(args, namespace)\n\t}\n\td.memory.QueryRow(query, args...).Scan(&epCount) //nolint:errcheck\n\n\t// Semantic count\n\tsemQuery := \"SELECT COUNT(*) FROM semantic_memories\"\n\tsemArgs := []any{}\n\tif namespace != \"\" {\n\t\tsemQuery += \" WHERE namespace = ?\"\n\t\tsemArgs = append(semArgs, namespace)\n\t}\n\td.memory.QueryRow(semQuery, semArgs...).Scan(&semCount) //nolint:errcheck\n\n\t// Top semantic\n\tsemRowQuery := \"SELECT concept, content, confidence, evidence_count, tags_json FROM semantic_memories\"\n\tif namespace != \"\" {\n\t\tsemRowQuery += \" WHERE namespace = ?\"\n\t}\n\tsemRowQuery += \" ORDER BY confidence DESC LIMIT 20\"\n\tsemRows, err := d.memory.Query(semRowQuery, semArgs...)\n\tif err != nil {\n\t\treturn epCount, semCount, nil, nil, err\n\t}\n\tdefer semRows.Close()\n\tvar concepts []*SemanticConcept\n\tfor semRows.Next() {\n\t\tc := &SemanticConcept{}\n\t\tvar tagsJSON string\n\t\tif err := semRows.Scan(&c.Concept, &c.Content, &c.Confidence, &c.EvidenceCount, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, nil, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &c.Tags) //nolint:errcheck\n\t\tconcepts = append(concepts, c)\n\t}\n\n\t// Recent episodes\n\tepRowQuery := \"SELECT episode_id, event_type, timestamp, cycle, importance, tags_json FROM episodes\"\n\tif namespace != \"\" {\n\t\tepRowQuery += \" WHERE namespace = ?\"\n\t}\n\tepRowQuery += \" ORDER BY timestamp DESC LIMIT 20\"\n\tepRows, err := d.memory.Query(epRowQuery, args...)\n\tif err != nil {\n\t\treturn epCount, semCount, concepts, nil, err\n\t}\n\tdefer epRows.Close()\n\tvar episodes []*EpisodeEntry\n\tfor epRows.Next() {\n\t\te := &EpisodeEntry{}\n\t\tvar tagsJSON string\n\t\tif err := epRows.Scan(&e.EpisodeID, &e.EventType, &e.Timestamp, &e.Cycle, &e.Importance, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, concepts, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &e.Tags) //nolint:errcheck\n\t\tepisodes = append(episodes, e)\n\t}\n\n\treturn epCount, semCount, concepts, episodes, nil\n}\n\n// SystemHealth computes overall system health from node states.\nfunc (d *DB) SystemHealth() (*SystemHealth, error) {\n\tnodes, err := d.AllNodes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn &SystemHealth{Status: \"no_nodes\"}, nil\n\t}\n\n\tvar totalHealth float64\n\tvar minHealth float64 = 1.0\n\tfor _, n := range nodes {\n\t\ttotalHealth += n.Health\n\t\tif n.Health < minHealth {\n\t\t\tminHealth = n.Health\n\t\t}\n\t}\n\tavgHealth := totalHealth / float64(len(nodes))\n\n\tissues, _ := d.GetIssues(\"open\")\n\topenCount := int64(len(issues))\n\tvar errorIssues int64\n\tfor _, i := range issues {\n\t\tif i.Severity == \"error\" {\n\t\t\terrorIssues++\n\t\t}\n\t}\n\n\tissuePenalty := float64(errorIssues) * 0.1\n\tif issuePenalty > 0.3 {\n\t\tissuePenalty = 0.3\n\t}\n\tcomposite := avgHealth - issuePenalty\n\tif composite < 0 {\n\t\tcomposite = 0\n\t}\n\n\tstatus := \"healthy\"\n\tif composite < 0.6 {\n\t\tstatus = \"critical\"\n\t} else if composite < 0.8 {\n\t\tstatus = \"degraded\"\n\t}\n\n\t// Uptime from first registered_at\n\tvar minRegAt float64 = float64(time.Now().Unix())\n\tfor _, n := range nodes {\n\t\tif n.RegisteredAt < minRegAt {\n\t\t\tminRegAt = n.RegisteredAt\n\t\t}\n\t}\n\tuptime := float64(time.Now().Unix()) - minRegAt\n\n\t// Total cycles\n\tvar totalCycles int64\n\td.state.QueryRow(\"SELECT COALESCE(MAX(cycle), 0) FROM node_executions\").Scan(&totalCycles) //nolint:errcheck\n\n\treturn &SystemHealth{\n\t\tStatus: status,\n\t\tCompositeScore: round3(composite),\n\t\tAvgNodeHealth: round3(avgHealth),\n\t\tNodeCount: int64(len(nodes)),\n\t\tOpenIssues: openCount,\n\t\tErrorIssues: errorIssues,\n\t\tUptimeSeconds: uptime,\n\t\tTotalCycles: totalCycles,\n\t}, nil\n}\n\nfunc round3(f float64) float64 {\n\treturn float64(int(f*1000+0.5)) / 1000\n}\n\n// ── Brain config types ─────────────────────────────────────────────────────────\n\n// BrainConfig is the LLM adapter config for a node — stored in Go-managed tables.\ntype BrainConfig struct {\n\tNodeID string\n\tProvider string\n\tModel string\n\tTemperature float64\n\tMaxTokens int64\n\tSystemPrompt string\n\tExtraConfig map[string]string\n\tUpdatedAt float64\n}\n\ntype BrainExecutionEntry struct {\n\tExecID string\n\tNodeID string\n\tProvider string\n\tModel string\n\tPromptTokens int64\n\tCompletionTokens int64\n\tLatencyMS float64\n\tSuccess bool\n\tErrorText string\n\tExecutedAt float64\n\tCycle int64\n}\n\n// ── Brain config DB methods ────────────────────────────────────────────────────\n\n// ensureBrainTables creates the Go-managed brain config tables if they don't exist.\n// Call this in New() before returning.\nfunc (d *DB) ensureBrainTables() error {\n\t_, err := d.state.Exec(`\n\t\tCREATE TABLE IF NOT EXISTS node_brain_config (\n\t\t\tnode_id TEXT PRIMARY KEY,\n\t\t\tprovider TEXT NOT NULL DEFAULT 'anthropic',\n\t\t\tmodel TEXT NOT NULL DEFAULT 'claude-sonnet-4-6',\n\t\t\ttemperature REAL NOT NULL DEFAULT 0.7,\n\t\t\tmax_tokens INTEGER NOT NULL DEFAULT 4096,\n\t\t\tsystem_prompt TEXT NOT NULL DEFAULT '',\n\t\t\textra_config TEXT NOT NULL DEFAULT '{}',\n\t\t\tupdated_at REAL NOT NULL DEFAULT (unixepoch('now'))\n\t\t);\n\t\tCREATE TABLE IF NOT EXISTS brain_execution_log (\n\t\t\texec_id TEXT PRIMARY KEY,\n\t\t\tnode_id TEXT NOT NULL,\n\t\t\tprovider TEXT NOT NULL,\n\t\t\tmodel TEXT NOT NULL,\n\t\t\tprompt_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tcompletion_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tlatency_ms REAL NOT NULL DEFAULT 0,\n\t\t\tsuccess INTEGER NOT NULL DEFAULT 1,\n\t\t\terror_text TEXT NOT NULL DEFAULT '',\n\t\t\texecuted_at REAL NOT NULL DEFAULT (unixepoch('now')),\n\t\t\tcycle INTEGER NOT NULL DEFAULT 0\n\t\t);`)\n\treturn err\n}\n\n// GetBrainConfig returns the brain config for a node (defaults if not set).\nfunc (d *DB) GetBrainConfig(nodeID string) (*BrainConfig, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, provider, model, temperature, max_tokens,\n\t\t system_prompt, extra_config, updated_at\n\t\tFROM node_brain_config WHERE node_id = ?`, nodeID)\n\tc := &BrainConfig{}\n\tvar extraJSON string\n\terr := row.Scan(&c.NodeID, &c.Provider, &c.Model, &c.Temperature,\n\t\t&c.MaxTokens, &c.SystemPrompt, &extraJSON, &c.UpdatedAt)\n\tif err == sql.ErrNoRows {\n\t\t// Return defaults\n\t\treturn &BrainConfig{\n\t\t\tNodeID: nodeID,\n\t\t\tProvider: \"anthropic\",\n\t\t\tModel: \"claude-sonnet-4-6\",\n\t\t\tTemperature: 0.7,\n\t\t\tMaxTokens: 4096,\n\t\t}, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(extraJSON), &c.ExtraConfig) //nolint:errcheck\n\treturn c, nil\n}\n\n// SetBrainConfig upserts the brain config for a node.\nfunc (d *DB) SetBrainConfig(c *BrainConfig) error {\n\textraJSON, _ := json.Marshal(c.ExtraConfig)\n\t_, err := d.state.Exec(`\n\t\tINSERT INTO node_brain_config\n\t\t\t(node_id, provider, model, temperature, max_tokens, system_prompt, extra_config, updated_at)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, unixepoch('now'))\n\t\tON CONFLICT(node_id) DO UPDATE SET\n\t\t\tprovider=excluded.provider, model=excluded.model,\n\t\t\ttemperature=excluded.temperature, max_tokens=excluded.max_tokens,\n\t\t\tsystem_prompt=excluded.system_prompt, extra_config=excluded.extra_config,\n\t\t\tupdated_at=excluded.updated_at`,\n\t\tc.NodeID, c.Provider, c.Model, c.Temperature,\n\t\tc.MaxTokens, c.SystemPrompt, string(extraJSON))\n\treturn err\n}\n\n// GetBrainHistory returns recent brain execution log for a node.\nfunc (d *DB) GetBrainHistory(nodeID string, limit int) ([]*BrainExecutionEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT exec_id, node_id, provider, model, prompt_tokens, completion_tokens,\n\t\t latency_ms, success, error_text, executed_at, cycle\n\t\tFROM brain_execution_log WHERE node_id = ?\n\t\tORDER BY executed_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*BrainExecutionEntry\n\tfor rows.Next() {\n\t\te := &BrainExecutionEntry{}\n\t\tvar success int\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.Provider, &e.Model,\n\t\t\t&e.PromptTokens, &e.CompletionTokens, &e.LatencyMS,\n\t\t\t&success, &e.ErrorText, &e.ExecutedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/db/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/ go.mod go.sum\ngit commit -m \"feat(db): add Go SQLite read layer for Omega state + memory DBs\"\n```\n\n---\n\n## Task 4: Go Connect-RPC Handler\n\n**Files:**\n- Create: `internal/handler/orchestrator.go`\n\n- [ ] **Step 1: Write `internal/handler/orchestrator.go`**\n\n```go\n// Package handler implements the OrchestratorService Connect-RPC handler.\npackage handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n)\n\n// Ensure interface satisfaction at compile time.\n// Buf's connectrpc/go plugin generates the handler interface in the same package as the proto types.\nvar _ omegav1.OrchestratorServiceHandler = (*OrchestratorHandler)(nil)\n\n// OrchestratorHandler implements OrchestratorService.\ntype OrchestratorHandler struct {\n\tdb *db.DB\n\tmu sync.Mutex\n\torchCmd *exec.Cmd\n\torchRunning bool\n}\n\n// New creates an OrchestratorHandler backed by the given DB.\nfunc New(database *db.DB) *OrchestratorHandler {\n\treturn &OrchestratorHandler{db: database}\n}\n\n// tsFromUnix converts a float64 Unix timestamp to a protobuf Timestamp.\nfunc tsFromUnix(u float64) *timestamppb.Timestamp {\n\tif u == 0 {\n\t\treturn nil\n\t}\n\tsec := int64(u)\n\tnsec := int32((u - float64(sec)) * 1e9)\n\treturn timestamppb.New(time.Unix(sec, int64(nsec)))\n}\n\nfunc tsFromUnixPtr(u *float64) *timestamppb.Timestamp {\n\tif u == nil {\n\t\treturn nil\n\t}\n\treturn tsFromUnix(*u)\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetHealth(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetHealthRequest],\n) (*connect.Response[omegav1.GetHealthResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetHealthResponse{\n\t\tHealth: dbHealthToProto(health),\n\t}), nil\n}\n\nfunc dbHealthToProto(h *db.SystemHealth) *omegav1.SystemHealth {\n\treturn &omegav1.SystemHealth{\n\t\tStatus: h.Status,\n\t\tCompositeScore: h.CompositeScore,\n\t\tAvgNodeHealth: h.AvgNodeHealth,\n\t\tNodeCount: h.NodeCount,\n\t\tOpenIssues: h.OpenIssues,\n\t\tErrorIssues: h.ErrorIssues,\n\t\tUptimeSeconds: h.UptimeSeconds,\n\t\tTotalCycles: h.TotalCycles,\n\t}\n}\n\n// ── Nodes ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListNodes(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodesRequest],\n) (*connect.Response[omegav1.ListNodesResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoNodes []*omegav1.Node\n\tfor _, n := range nodes {\n\t\tprotoNodes = append(protoNodes, dbNodeToProto(n))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodesResponse{Nodes: protoNodes}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNode(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeRequest],\n) (*connect.Response[omegav1.GetNodeResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\texecs, _ := h.db.GetExecutions(req.Msg.NodeId, 10)\n\timps, _ := h.db.GetImprovements(req.Msg.NodeId, 20)\n\thistory, _ := h.db.LatencyHistory(req.Msg.NodeId, 100)\n\n\tresp := &omegav1.GetNodeResponse{\n\t\tNode: dbNodeToProto(n),\n\t\tImprovements: dbImpsToProto(imps),\n\t}\n\tfor _, e := range execs {\n\t\tresp.RecentExecutions = append(resp.RecentExecutions, dbExecToProto(e))\n\t}\n\tfor _, p := range history {\n\t\tresp.LatencyHistory = append(resp.LatencyHistory, &omegav1.LatencyPoint{\n\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\tDurationMs: p.DurationMS,\n\t\t\tSuccess: p.Success,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) ListNodeExecutions(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodeExecutionsRequest],\n) (*connect.Response[omegav1.ListNodeExecutionsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\texecs, err := h.db.GetExecutions(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoExecs []*omegav1.ExecutionRecord\n\tfor _, e := range execs {\n\t\tprotoExecs = append(protoExecs, dbExecToProto(e))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodeExecutionsResponse{Executions: protoExecs}), nil\n}\n\nfunc dbNodeToProto(n *db.Node) *omegav1.Node {\n\tnode := &omegav1.Node{\n\t\tNodeId: n.NodeID,\n\t\tName: n.Name,\n\t\tVersion: n.Version,\n\t\tCapabilities: n.Capabilities,\n\t\tHealth: n.Health,\n\t\tStatus: n.Status,\n\t\tRegisteredAt: tsFromUnix(n.RegisteredAt),\n\t\tLastUpdated: tsFromUnix(n.LastUpdated),\n\t\tExecutionsTotal: n.ExecutionsTotal,\n\t\tErrorRate: n.ErrorRate,\n\t\tAvgLatencyMs: n.AvgLatencyMS,\n\t\tP95LatencyMs: n.P95LatencyMS,\n\t\tImprovementCount: n.ImprovementCount,\n\t}\n\tif n.LastExecution != nil {\n\t\tnode.LastExecution = dbExecToProto(n.LastExecution)\n\t}\n\treturn node\n}\n\nfunc dbExecToProto(e *db.Execution) *omegav1.ExecutionRecord {\n\trec := &omegav1.ExecutionRecord{\n\t\tExecId: e.ExecID,\n\t\tNodeId: e.NodeID,\n\t\tNodeName: e.NodeName,\n\t\tTraceId: e.TraceID,\n\t\tSpanId: e.SpanID,\n\t\tAction: e.Action,\n\t\tStartedAt: tsFromUnix(e.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(e.EndedAt),\n\t\tSuccess: e.Success,\n\t\tErrorText: e.ErrorText,\n\t\tMetrics: e.Metrics,\n\t\tCycle: e.Cycle,\n\t}\n\tif e.DurationMS != nil {\n\t\trec.DurationMs = *e.DurationMS\n\t}\n\treturn rec\n}\n\n// ── Traces ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListTraces(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListTracesRequest],\n) (*connect.Response[omegav1.ListTracesResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 20\n\t}\n\ttraces, err := h.db.RecentTraces(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoTraces []*omegav1.TraceSummary\n\tfor _, t := range traces {\n\t\tprotoTraces = append(protoTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(&omegav1.ListTracesResponse{Traces: protoTraces}), nil\n}\n\nfunc (h *OrchestratorHandler) GetTrace(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetTraceRequest],\n) (*connect.Response[omegav1.GetTraceResponse], error) {\n\tspans, err := h.db.GetTraceSpans(req.Msg.TraceId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tif len(spans) == 0 {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"trace not found: %s\", req.Msg.TraceId))\n\t}\n\tvar protoSpans []*omegav1.Span\n\tfor _, s := range spans {\n\t\tprotoSpans = append(protoSpans, dbSpanToProto(s))\n\t}\n\treturn connect.NewResponse(&omegav1.GetTraceResponse{Spans: protoSpans}), nil\n}\n\nfunc dbTraceSummaryToProto(t *db.TraceSummary) *omegav1.TraceSummary {\n\treturn &omegav1.TraceSummary{\n\t\tTraceId: t.TraceID,\n\t\tTraceStarted: tsFromUnix(t.TraceStarted),\n\t\tTraceEnded: tsFromUnixPtr(t.TraceEnded),\n\t\tTotalDurationMs: t.TotalDurationMS,\n\t\tSpanCount: t.SpanCount,\n\t\tErrorSpans: t.ErrorSpans,\n\t\tCycle: t.Cycle,\n\t}\n}\n\nfunc dbSpanToProto(s *db.Span) *omegav1.Span {\n\tspan := &omegav1.Span{\n\t\tSpanId: s.SpanID,\n\t\tTraceId: s.TraceID,\n\t\tParentSpanId: s.ParentSpanID,\n\t\tNodeId: s.NodeID,\n\t\tNodeName: s.NodeName,\n\t\tOperation: s.Operation,\n\t\tStartedAt: tsFromUnix(s.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(s.EndedAt),\n\t\tStatus: s.Status,\n\t\tCycle: s.Cycle,\n\t}\n\tif s.DurationMS != nil {\n\t\tspan.DurationMs = *s.DurationMS\n\t}\n\treturn span\n}\n\n// ── Metrics ───────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetMetrics(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsRequest],\n) (*connect.Response[omegav1.GetMetricsResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tcosts, _ := h.db.GetCosts()\n\tissues, _ := h.db.GetIssues(\"open\")\n\ttraces, _ := h.db.RecentTraces(5)\n\n\tresp := &omegav1.GetMetricsResponse{System: dbHealthToProto(health)}\n\tfor _, n := range nodes {\n\t\tresp.Nodes = append(resp.Nodes, dbNodeToProto(n))\n\t}\n\tfor _, c := range costs {\n\t\tresp.Costs = append(resp.Costs, dbCostToProto(c))\n\t}\n\tfor _, i := range issues {\n\t\tresp.OpenIssues = append(resp.OpenIssues, dbIssueToProto(i))\n\t}\n\tfor _, t := range traces {\n\t\tresp.RecentTraces = append(resp.RecentTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) GetMetricsHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsHistoryRequest],\n) (*connect.Response[omegav1.GetMetricsHistoryResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar histories []*omegav1.NodeLatencyHistory\n\tfor _, n := range nodes {\n\t\tif req.Msg.NodeId != \"\" && n.NodeID != req.Msg.NodeId {\n\t\t\tcontinue\n\t\t}\n\t\thistory, _ := h.db.LatencyHistory(n.NodeID, 100)\n\t\tnh := &omegav1.NodeLatencyHistory{NodeId: n.NodeID, Name: n.Name}\n\t\tfor _, p := range history {\n\t\t\tnh.History = append(nh.History, &omegav1.LatencyPoint{\n\t\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\t\tDurationMs: p.DurationMS,\n\t\t\t\tSuccess: p.Success,\n\t\t\t})\n\t\t}\n\t\thistories = append(histories, nh)\n\t}\n\treturn connect.NewResponse(&omegav1.GetMetricsHistoryResponse{NodeHistories: histories}), nil\n}\n\n// ── Issues ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListIssues(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListIssuesRequest],\n) (*connect.Response[omegav1.ListIssuesResponse], error) {\n\tissues, err := h.db.GetIssues(req.Msg.StateFilter)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoIssues []*omegav1.Issue\n\tfor _, i := range issues {\n\t\tprotoIssues = append(protoIssues, dbIssueToProto(i))\n\t}\n\treturn connect.NewResponse(&omegav1.ListIssuesResponse{Issues: protoIssues}), nil\n}\n\nfunc dbIssueToProto(i *db.Issue) *omegav1.Issue {\n\tissue := &omegav1.Issue{\n\t\tIssueId: i.IssueID,\n\t\tDetector: i.Detector,\n\t\tSeverity: i.Severity,\n\t\tDescription: i.Description,\n\t\tState: i.State,\n\t\tOpenedAt: tsFromUnix(i.OpenedAt),\n\t\tResolvedAt: tsFromUnixPtr(i.ResolvedAt),\n\t\tCycleOpened: i.CycleOpened,\n\t}\n\tif i.CycleResolved != nil {\n\t\tissue.CycleResolved = *i.CycleResolved\n\t}\n\treturn issue\n}\n\n// ── Activity ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListActivity(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListActivityRequest],\n) (*connect.Response[omegav1.ListActivityResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.RecentActivity(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.ActivityEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.ActivityEntry{\n\t\t\tLogId: e.LogID,\n\t\t\tActionType: e.ActionType,\n\t\t\tEntityType: e.EntityType,\n\t\t\tEntityId: e.EntityID,\n\t\t\tRecordedAt: tsFromUnix(e.RecordedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.ListActivityResponse{Entries: protoEntries}), nil\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListImprovements(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListImprovementsRequest],\n) (*connect.Response[omegav1.ListImprovementsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\timps, err := h.db.GetImprovements(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.ListImprovementsResponse{\n\t\tImprovements: dbImpsToProto(imps),\n\t}), nil\n}\n\nfunc dbImpsToProto(imps []*db.Improvement) []*omegav1.ImprovementRecord {\n\tvar result []*omegav1.ImprovementRecord\n\tfor _, imp := range imps {\n\t\tresult = append(result, &omegav1.ImprovementRecord{\n\t\t\tImproveId: imp.ImproveID,\n\t\t\tNodeId: imp.NodeID,\n\t\t\tNodeName: imp.NodeName,\n\t\t\tFromVersion: imp.FromVersion,\n\t\t\tToVersion: imp.ToVersion,\n\t\t\tTriggeredBy: imp.TriggeredBy,\n\t\t\tRecordedAt: tsFromUnix(imp.RecordedAt),\n\t\t\tCycle: imp.Cycle,\n\t\t\tBeforeMetrics: imp.BeforeMetrics,\n\t\t\tAfterMetrics: imp.AfterMetrics,\n\t\t})\n\t}\n\treturn result\n}\n\n// ── Memory ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetNodeMemory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeMemoryRequest],\n) (*connect.Response[omegav1.GetNodeMemoryResponse], error) {\n\t// Resolve node name for namespace\n\tvar namespace string\n\tif req.Msg.NodeId != \"\" {\n\t\tn, err := h.db.GetNode(req.Msg.NodeId)\n\t\tif err != nil {\n\t\t\treturn nil, connect.NewError(connect.CodeNotFound, err)\n\t\t}\n\t\tnamespace = n.Name\n\t}\n\n\tepCount, semCount, concepts, episodes, err := h.db.GetMemoryStats(namespace)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\tresp := &omegav1.GetNodeMemoryResponse{\n\t\tNamespace: namespace,\n\t\tEpisodicCount: epCount,\n\t\tSemanticCount: semCount,\n\t}\n\tfor _, c := range concepts {\n\t\tresp.TopSemantic = append(resp.TopSemantic, &omegav1.SemanticConcept{\n\t\t\tConcept: c.Concept,\n\t\t\tContent: c.Content,\n\t\t\tConfidence: c.Confidence,\n\t\t\tEvidenceCount: c.EvidenceCount,\n\t\t\tTags: c.Tags,\n\t\t})\n\t}\n\tfor _, e := range episodes {\n\t\tresp.RecentEpisodes = append(resp.RecentEpisodes, &omegav1.EpisodeEntry{\n\t\t\tEpisodeId: e.EpisodeID,\n\t\t\tEventType: e.EventType,\n\t\t\tTimestamp: tsFromUnix(e.Timestamp),\n\t\t\tCycle: e.Cycle,\n\t\t\tImportance: e.Importance,\n\t\t\tTags: e.Tags,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetConvergence(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetConvergenceRequest],\n) (*connect.Response[omegav1.GetConvergenceResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 100\n\t}\n\tpoints, err := h.db.GetConvergence(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoPoints []*omegav1.ConvergencePoint\n\tfor _, p := range points {\n\t\tprotoPoints = append(protoPoints, &omegav1.ConvergencePoint{\n\t\t\tCycle: p.Cycle,\n\t\t\tTimestamp: tsFromUnix(p.Timestamp),\n\t\t\tScore: p.Score,\n\t\t\tPipelineMs: p.PipelineMS,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetConvergenceResponse{Points: protoPoints}), nil\n}\n\n// ── Costs ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetCosts(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetCostsRequest],\n) (*connect.Response[omegav1.GetCostsResponse], error) {\n\tcosts, err := h.db.GetCosts()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoCosts []*omegav1.CostEntry\n\tfor _, c := range costs {\n\t\tprotoCosts = append(protoCosts, dbCostToProto(c))\n\t}\n\treturn connect.NewResponse(&omegav1.GetCostsResponse{Costs: protoCosts}), nil\n}\n\nfunc dbCostToProto(c *db.CostEntry) *omegav1.CostEntry {\n\treturn &omegav1.CostEntry{\n\t\tProvider: c.Provider,\n\t\tNodeId: c.NodeID,\n\t\tCalls: c.Calls,\n\t\tTotalMs: c.TotalMS,\n\t\tTotalCostUsd: c.TotalCostUSD,\n\t}\n}\n\n// ── Feedback ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) SubmitFeedback(\n\tctx context.Context,\n\treq *connect.Request[omegav1.SubmitFeedbackRequest],\n) (*connect.Response[omegav1.SubmitFeedbackResponse], error) {\n\t// Shell out to Python to store feedback in MemoryKernel\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-c\", fmt.Sprintf(`\nfrom omega.core.memory import MemoryKernel\nm = MemoryKernel(db_path='/tmp/omega_victoria_memory.db')\nm.store_episode('human_feedback', {'text': %q, 'target_node': %q}, tags=['feedback','human'], importance=0.9)\nprint('ok')\n`, req.Msg.Text, req.Msg.TargetNode))\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"feedback storage failed: %w\", err))\n\t}\n\treturn connect.NewResponse(&omegav1.SubmitFeedbackResponse{Ok: true}), nil\n}\n\n// ── Orchestrator control ──────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StartOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StartOrchestratorRequest],\n) (*connect.Response[omegav1.StartOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif h.orchRunning {\n\t\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\t\tStarted: false, Message: \"already running\",\n\t\t}), nil\n\t}\n\theartbeat := req.Msg.HeartbeatSecs\n\tif heartbeat <= 0 {\n\t\theartbeat = 120\n\t}\n\tcmd := exec.Command(\"python3\", \"-m\", \"omega.examples.victoria_main\",\n\t\t\"--heartbeat\", fmt.Sprintf(\"%d\", heartbeat))\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"start orchestrator: %w\", err))\n\t}\n\th.orchCmd = cmd\n\th.orchRunning = true\n\t// Watch for exit\n\tgo func() {\n\t\tcmd.Wait() //nolint:errcheck\n\t\th.mu.Lock()\n\t\th.orchRunning = false\n\t\th.orchCmd = nil\n\t\th.mu.Unlock()\n\t}()\n\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\tStarted: true, Message: \"started\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) StopOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StopOrchestratorRequest],\n) (*connect.Response[omegav1.StopOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif !h.orchRunning || h.orchCmd == nil {\n\t\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\t\tStopped: false, Message: \"not running\",\n\t\t}), nil\n\t}\n\tif err := h.orchCmd.Process.Kill(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\th.orchRunning = false\n\th.orchCmd = nil\n\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\tStopped: true, Message: \"stopped\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) TriggerHeartbeat(\n\tctx context.Context,\n\treq *connect.Request[omegav1.TriggerHeartbeatRequest],\n) (*connect.Response[omegav1.TriggerHeartbeatResponse], error) {\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-m\", \"omega.examples.victoria_main\", \"--once\")\n\tgo cmd.Run() //nolint:errcheck\n\treturn connect.NewResponse(&omegav1.TriggerHeartbeatResponse{\n\t\tTriggered: true, Message: \"triggered single heartbeat\",\n\t}), nil\n}\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StreamEvents(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StreamEventsRequest],\n\tstream *connect.ServerStream[omegav1.EventResponse],\n) error {\n\tinterval := time.Duration(req.Msg.PollIntervalMs) * time.Millisecond\n\tif interval < 500*time.Millisecond {\n\t\tinterval = 2 * time.Second\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase <-ticker.C:\n\t\t\thealth, err := h.db.SystemHealth()\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := stream.Send(&omegav1.EventResponse{\n\t\t\t\tType: \"health_update\",\n\t\t\t\tPayload: &omegav1.EventResponse_HealthUpdate{\n\t\t\t\t\tHealthUpdate: dbHealthToProto(health),\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ── Brain config RPCs ─────────────────────────────────────────────────────────\n\n// knownProviders lists all supported LLM providers and their available models.\nvar knownProviders = []*omegav1.ProviderInfo{\n\t{Id: \"anthropic\", DisplayName: \"Anthropic\", Models: []string{\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"openai\", DisplayName: \"OpenAI\", Models: []string{\"gpt-4o\", \"gpt-4o-mini\", \"o1\", \"o3-mini\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"google\", DisplayName: \"Google\", Models: []string{\"gemini-2.0-flash\", \"gemini-2.0-pro\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"deepseek\", DisplayName: \"DeepSeek\", Models: []string{\"deepseek-chat\", \"deepseek-reasoner\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"ollama\", DisplayName: \"Ollama (local)\", Models: []string{\"llama3.3\", \"qwen2.5-coder\", \"mistral\"}, RequiresApiKey: false, Available: true},\n}\n\nfunc (h *OrchestratorHandler) ListAvailableProviders(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListAvailableProvidersRequest],\n) (*connect.Response[omegav1.ListAvailableProvidersResponse], error) {\n\treturn connect.NewResponse(&omegav1.ListAvailableProvidersResponse{\n\t\tProviders: knownProviders,\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNodeConfig(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeConfigRequest],\n) (*connect.Response[omegav1.GetNodeConfigResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\tbrain, err := h.db.GetBrainConfig(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetNodeConfigResponse{\n\t\tConfig: dbNodeConfigToProto(n, brain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) UpdateNodeBrain(\n\tctx context.Context,\n\treq *connect.Request[omegav1.UpdateNodeBrainRequest],\n) (*connect.Response[omegav1.UpdateNodeBrainResponse], error) {\n\tbrain := &db.BrainConfig{\n\t\tNodeID: req.Msg.NodeId,\n\t\tProvider: req.Msg.Brain.Provider,\n\t\tModel: req.Msg.Brain.Model,\n\t\tTemperature: req.Msg.Brain.Temperature,\n\t\tMaxTokens: int64(req.Msg.Brain.MaxTokens),\n\t\tSystemPrompt: req.Msg.Brain.SystemPrompt,\n\t\tExtraConfig: req.Msg.Brain.ExtraConfig,\n\t}\n\tif err := h.db.SetBrainConfig(brain); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tn, _ := h.db.GetNode(req.Msg.NodeId)\n\tupdatedBrain, _ := h.db.GetBrainConfig(req.Msg.NodeId)\n\treturn connect.NewResponse(&omegav1.UpdateNodeBrainResponse{\n\t\tConfig: dbNodeConfigToProto(n, updatedBrain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetBrainExecutionHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetBrainExecutionHistoryRequest],\n) (*connect.Response[omegav1.GetBrainExecutionHistoryResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.GetBrainHistory(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.BrainExecutionEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.BrainExecutionEntry{\n\t\t\tExecId: e.ExecID,\n\t\t\tNodeId: e.NodeID,\n\t\t\tProvider: e.Provider,\n\t\t\tModel: e.Model,\n\t\t\tPromptTokens: e.PromptTokens,\n\t\t\tCompletionTokens: e.CompletionTokens,\n\t\t\tLatencyMs: e.LatencyMS,\n\t\t\tSuccess: e.Success,\n\t\t\tErrorText: e.ErrorText,\n\t\t\tExecutedAt: tsFromUnix(e.ExecutedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetBrainExecutionHistoryResponse{Entries: protoEntries}), nil\n}\n\nfunc dbNodeConfigToProto(n *db.Node, brain *db.BrainConfig) *omegav1.NodeConfig {\n\tconfig := &omegav1.NodeConfig{\n\t\tNodeId: n.NodeID,\n\t\tNodeType: n.Name,\n\t\tVersion: n.Version,\n\t\tBrain: &omegav1.BrainConfig{\n\t\t\tProvider: brain.Provider,\n\t\t\tModel: brain.Model,\n\t\t\tTemperature: brain.Temperature,\n\t\t\tMaxTokens: int32(brain.MaxTokens),\n\t\t\tSystemPrompt: brain.SystemPrompt,\n\t\t\tExtraConfig: brain.ExtraConfig,\n\t\t},\n\t\tUpdatedAt: tsFromUnix(brain.UpdatedAt),\n\t}\n\treturn config\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/handler/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/handler/\ngit commit -m \"feat(handler): implement OrchestratorService + brain config Connect-RPC handlers\"\n```\n\n---\n\n## Task 5: Go API Server Entry Point\n\n**Files:**\n- Create: `cmd/omega-api/main.go`\n\n- [ ] **Step 1: Write `cmd/omega-api/main.go`**\n\n```go\n// Command omega-api serves the Omega dashboard Connect-RPC API.\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"connectrpc.com/connect\"\n\tconnectcors \"connectrpc.com/connect/cors\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n\t\"github.com/benebsworth/omega/internal/handler\"\n)\n\nfunc main() {\n\tdatabase, err := db.New(db.StateDBPath, db.MemoryDBPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"open DB: %v\", err)\n\t}\n\tdefer database.Close()\n\n\th := handler.New(database)\n\tmux := http.NewServeMux()\n\n\t// Buf's connectrpc/go plugin generates NewOrchestratorServiceHandler in the same package.\n\tpath, svcHandler := omegav1.NewOrchestratorServiceHandler(h,\n\t\tconnect.WithCompressMinBytes(1024),\n\t)\n\tmux.Handle(path, svcHandler)\n\n\t// CORS — use connectrpc.com/connect/cors helpers to include streaming headers.\n\tcorsHandler := withCORS(mux)\n\n\taddr := \":8080\"\n\tlog.Printf(\"Omega API listening on %s (Connect-RPC)\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, h2c.NewHandler(corsHandler, &http2.Server{})))\n}\n\nfunc withCORS(h http.Handler) http.Handler {\n\tallowedHeaders := append(connectcors.AllowedHeaders(), \"X-User-Agent\")\n\texposedHeaders := connectcors.ExposedHeaders()\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", strings.Join(allowedHeaders, \", \"))\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", strings.Join(exposedHeaders, \", \"))\n\t\tif r.Method == http.MethodOptions {\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\treturn\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t})\n}\n```\n\n- [ ] **Step 2: Build and verify**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api/...\n```\n\nExpected: produces `./omega-api` (or similar) without errors.\n\n- [ ] **Step 3: Smoke test — start server and verify it responds**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\ncurl -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" \\\n -d '{}' | python3 -m json.tool\nkill %1\n```\n\nExpected: JSON response with health data (or empty `{}` if DB doesn't exist yet).\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add cmd/\ngit commit -m \"feat(cmd): add omega-api Connect-RPC server entry point\"\n```\n\n---\n\n## Task 6: Frontend Scaffold + Connect-ES Setup\n\n**Files:**\n- Create: `dashboard/` — Vite React TS project\n\n- [ ] **Step 1: Scaffold Vite project at repo root**\n\n```bash\ncd /Users/benebsworth/projects/omega\nnpm create vite@latest dashboard -- --template react-ts\n```\n\n- [ ] **Step 2: Install dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm install\nnpm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf\nnpm install react-router-dom recharts lucide-react\nnpm install -D tailwindcss postcss autoprefixer @types/node\nnpx tailwindcss init -p\n```\n\n- [ ] **Step 3: Configure Tailwind — `tailwind.config.js`**\n\n```js\n/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n theme: {\n extend: {\n colors: {\n surface: {\n 900: \"#0f1117\",\n 800: \"#161b27\",\n 700: \"#1e2533\",\n 600: \"#252d3d\",\n },\n },\n },\n },\n plugins: [],\n}\n```\n\n- [ ] **Step 4: Replace `src/index.css`**\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n- [ ] **Step 5: Configure Vite proxy — `vite.config.ts`**\n\n```ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n port: 5173,\n proxy: {\n // Proxy all Connect-RPC calls to the Go server\n \"/omega.v1\": {\n target: \"http://localhost:8080\",\n changeOrigin: true,\n },\n },\n },\n});\n```\n\n- [ ] **Step 6: Write `src/client.ts` — Connect transport + service client**\n\n```typescript\nimport { createConnectTransport } from \"@connectrpc/connect-web\";\nimport { createClient } from \"@connectrpc/connect\";\nimport { OrchestratorService } from \"./gen/omega/v1/omega_service_pb\";\n\n// In dev, Vite proxies /omega.v1/* → http://localhost:8080\n// In production, VITE_API_URL should point to the Go server\nconst baseUrl = import.meta.env.VITE_API_URL ?? \"\";\n\nexport const transport = createConnectTransport({\n baseUrl,\n // Use the Connect protocol (works over HTTP/1.1 without gRPC-web)\n});\n\nexport const client = createClient(OrchestratorService, transport);\n```\n\n- [ ] **Step 7: Commit scaffold**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/\ngit commit -m \"feat(dashboard): scaffold Vite React TS with Connect-ES + Tailwind\"\n```\n\n---\n\n## Task 7: Layout Components\n\n**Files:**\n- Create: `dashboard/src/components/layout/Sidebar.tsx`\n- Create: `dashboard/src/components/layout/Header.tsx`\n- Create: `dashboard/src/App.tsx`\n\n- [ ] **Step 1: Write `Sidebar.tsx`**\n\n```tsx\nimport { NavLink } from \"react-router-dom\";\nimport {\n LayoutDashboard, Server, GitBranch, BarChart2,\n AlertTriangle, Brain, TrendingUp,\n} from \"lucide-react\";\n\nconst NAV = [\n { to: \"/\", icon: LayoutDashboard, label: \"Dashboard\" },\n { to: \"/nodes\", icon: Server, label: \"Nodes\" },\n { to: \"/traces\", icon: GitBranch, label: \"Traces\" },\n { to: \"/metrics\", icon: BarChart2, label: \"Metrics\" },\n { to: \"/issues\", icon: AlertTriangle, label: \"Issues\" },\n { to: \"/memory\", icon: Brain, label: \"Memory\" },\n { to: \"/convergence\", icon: TrendingUp, label: \"Convergence\" },\n];\n\nexport default function Sidebar() {\n return (\n \n );\n}\n```\n\n- [ ] **Step 2: Write `Header.tsx`**\n\n```tsx\nimport { useState } from \"react\";\nimport { client } from \"../../client\";\nimport { Play, Square, Zap } from \"lucide-react\";\n\ninterface HeaderProps {\n systemStatus?: string;\n connected?: boolean;\n}\n\nexport default function Header({ systemStatus, connected }: HeaderProps) {\n const [loading, setLoading] = useState(null);\n\n const statusColor = {\n healthy: \"bg-green-500\",\n degraded: \"bg-yellow-500\",\n critical: \"bg-red-500\",\n no_nodes: \"bg-gray-500\",\n }[systemStatus ?? \"no_nodes\"] ?? \"bg-gray-500\";\n\n async function action(name: \"start\" | \"stop\" | \"trigger\") {\n setLoading(name);\n try {\n if (name === \"start\") await client.startOrchestrator({ heartbeatSecs: 120 });\n else if (name === \"stop\") await client.stopOrchestrator({});\n else await client.triggerHeartbeat({});\n } finally {\n setLoading(null);\n }\n }\n\n return (\n
\n
\n \n \n {systemStatus ?? \"unknown\"}\n \n {connected !== undefined && (\n \n {connected ? \"streaming\" : \"offline\"}\n \n )}\n
\n
\n {([\"start\", \"trigger\", \"stop\"] as const).map((name) => (\n action(name)}\n disabled={!!loading}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors disabled:opacity-50 ${\n name === \"start\" ? \"bg-green-700 hover:bg-green-600\" :\n name === \"trigger\" ? \"bg-indigo-700 hover:bg-indigo-600\" :\n \"bg-red-800 hover:bg-red-700\"\n }`}\n >\n {name === \"start\" ? : name === \"stop\" ? : }\n {name.charAt(0).toUpperCase() + name.slice(1)}\n \n ))}\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Write `App.tsx`**\n\n```tsx\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Sidebar from \"./components/layout/Sidebar\";\nimport Header from \"./components/layout/Header\";\nimport { client } from \"./client\";\nimport type { SystemHealth } from \"./gen/omega/v1/types_pb\";\nimport Dashboard from \"./pages/Dashboard\";\nimport Nodes from \"./pages/Nodes\";\nimport Traces from \"./pages/Traces\";\nimport Metrics from \"./pages/Metrics\";\nimport Issues from \"./pages/Issues\";\nimport Memory from \"./pages/Memory\";\nimport Convergence from \"./pages/Convergence\";\n\nexport default function App() {\n const [health, setHealth] = useState(null);\n const [streamConnected, setStreamConnected] = useState(false);\n const abortRef = useRef(null);\n\n useEffect(() => {\n startStream();\n return () => abortRef.current?.abort();\n }, []);\n\n async function startStream() {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n setStreamConnected(true);\n for await (const event of client.streamEvents(\n { pollIntervalMs: 2000 },\n { signal: controller.signal }\n )) {\n if (event.payload.case === \"healthUpdate\") {\n setHealth(event.payload.value);\n }\n }\n } catch {\n setStreamConnected(false);\n // Reconnect after 3s\n setTimeout(startStream, 3000);\n }\n }\n\n return (\n \n
\n \n
\n \n
\n \n } />\n } />\n } />\n } />\n } />\n } />\n } />\n } />\n \n
\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 4: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/src/\ngit commit -m \"feat(dashboard): add layout + App router with server-streaming health\"\n```\n\n---\n\n## Task 8: Dashboard Page\n\n**Files:**\n- Create: `dashboard/src/pages/Dashboard.tsx`\n- Create: `dashboard/src/components/NodeCard.tsx`\n\n- [ ] **Step 1: Write `NodeCard.tsx`**\n\n```tsx\nimport { LineChart, Line, ResponsiveContainer } from \"recharts\";\nimport type { Node } from \"../gen/omega/v1/types_pb\";\n\nfunction healthColor(h: number) {\n if (h >= 0.8) return \"border-green-600 bg-green-900/20\";\n if (h >= 0.6) return \"border-yellow-600 bg-yellow-900/20\";\n return \"border-red-600 bg-red-900/20\";\n}\n\nfunction healthText(h: number) {\n if (h >= 0.8) return \"text-green-400\";\n if (h >= 0.6) return \"text-yellow-400\";\n return \"text-red-400\";\n}\n\ninterface NodeCardProps { node: Node }\n\nexport default function NodeCard({ node }: NodeCardProps) {\n const sparkData = node.latencyHistory.slice(-20).map((p) => ({ v: p.durationMs }));\n const lastExecAgo = node.lastExecution?.startedAt\n ? Math.round((Date.now() / 1000 - Number(node.lastExecution.startedAt.seconds)) / 60)\n : null;\n\n return (\n
\n
\n
\n

{node.name}

\n

v{node.version}

\n
\n \n {(node.health * 100).toFixed(0)}%\n \n
\n {sparkData.length > 1 && (\n
\n \n \n = 0.8 ? \"#4ade80\" : node.health >= 0.6 ? \"#facc15\" : \"#f87171\"}\n dot={false} strokeWidth={1.5} />\n \n \n
\n )}\n
\n err: {(node.errorRate * 100).toFixed(1)}%\n {lastExecAgo !== null ? `${lastExecAgo}m ago` : \"never\"}\n
\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Dashboard.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport NodeCard from \"../components/NodeCard\";\nimport type { SystemHealth, Node, ActivityEntry } from \"../gen/omega/v1/types_pb\";\n\ninterface DashboardProps { health: SystemHealth | null }\n\nexport default function Dashboard({ health }: DashboardProps) {\n const [nodes, setNodes] = useState([]);\n const [activity, setActivity] = useState([]);\n\n useEffect(() => {\n client.listNodes({}).then((r) => setNodes(r.nodes)).catch(console.error);\n client.listActivity({ limit: 20 }).then((r) => setActivity(r.entries)).catch(console.error);\n }, []);\n\n const statusColor: Record = {\n healthy: \"text-green-400\",\n degraded: \"text-yellow-400\",\n critical: \"text-red-400\",\n };\n\n return (\n
\n {/* System stats */}\n
\n {[\n { label: \"Status\", value: health?.status ?? \"—\", cls: statusColor[health?.status ?? \"\"] ?? \"text-white\" },\n { label: \"Health Score\", value: health ? `${(health.compositeScore * 100).toFixed(1)}%` : \"—\", cls: \"text-white\" },\n { label: \"Total Cycles\", value: String(health?.totalCycles ?? \"—\"), cls: \"text-white\" },\n { label: \"Open Issues\", value: String(health?.openIssues ?? \"—\"), cls: (health?.openIssues ?? 0) > 0 ? \"text-red-400\" : \"text-white\" },\n ].map(({ label, value, cls }) => (\n
\n

{label}

\n

{value}

\n
\n ))}\n
\n\n {/* Node grid */}\n
\n

\n Nodes ({nodes.length})\n

\n
\n {nodes.map((n) => )}\n
\n {nodes.length === 0 && (\n

\n No nodes registered — run the orchestrator first.\n

\n )}\n
\n\n {/* Activity feed */}\n
\n

\n Recent Activity\n

\n
\n {activity.map((evt) => (\n
\n \n {evt.recordedAt\n ? new Date(Number(evt.recordedAt.seconds) * 1000).toLocaleTimeString()\n : \"—\"}\n \n {evt.actionType}\n {evt.entityId}\n
\n ))}\n {activity.length === 0 && (\n

No activity yet.

\n )}\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/Dashboard.tsx dashboard/src/components/NodeCard.tsx\ngit commit -m \"feat(dashboard): add Dashboard page using Connect-ES typed client\"\n```\n\n---\n\n## Task 9: Traces Page with Waterfall\n\n**Files:**\n- Create: `dashboard/src/components/TraceWaterfall.tsx`\n- Create: `dashboard/src/pages/Traces.tsx`\n\n- [ ] **Step 1: Write `TraceWaterfall.tsx`**\n\n```tsx\nimport type { Span } from \"../gen/omega/v1/types_pb\";\n\ninterface TraceWaterfallProps { spans: Span[] }\n\nexport default function TraceWaterfall({ spans }: TraceWaterfallProps) {\n if (!spans.length) return

No spans.

;\n\n const sorted = [...spans].sort(\n (a, b) => Number(a.startedAt?.seconds ?? 0) - Number(b.startedAt?.seconds ?? 0)\n );\n const minTs = Number(sorted[0]?.startedAt?.seconds ?? 0);\n const maxTs = sorted.reduce(\n (m, s) => Math.max(m, Number(s.endedAt?.seconds ?? s.startedAt?.seconds ?? 0)), 0\n );\n const totalMs = Math.max(1, (maxTs - minTs) * 1000);\n\n const depthMap = new Map();\n for (const span of sorted) {\n depthMap.set(span.spanId, span.parentSpanId ? (depthMap.get(span.parentSpanId) ?? 0) + 1 : 0);\n }\n\n return (\n
\n {sorted.map((span) => {\n const depth = depthMap.get(span.spanId) ?? 0;\n const offsetMs = (Number(span.startedAt?.seconds ?? 0) - minTs) * 1000;\n const durMs = span.durationMs;\n const leftPct = (offsetMs / totalMs) * 100;\n const widthPct = Math.max(0.5, (durMs / totalMs) * 100);\n const isErr = span.status === \"error\";\n\n return (\n
\n
\n {isErr ? \"✗\" : \"✓\"}{\" \"}\n {span.nodeName || span.operation}\n
\n
\n \n
\n
{durMs.toFixed(0)}ms
\n
\n );\n })}\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Traces.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport TraceWaterfall from \"../components/TraceWaterfall\";\nimport type { TraceSummary, Span } from \"../gen/omega/v1/types_pb\";\n\nexport default function Traces() {\n const [traces, setTraces] = useState([]);\n const [selected, setSelected] = useState(null);\n const [spans, setSpans] = useState([]);\n\n useEffect(() => {\n client.listTraces({ limit: 30 }).then((r) => setTraces(r.traces)).catch(console.error);\n }, []);\n\n function selectTrace(id: string) {\n setSelected(id);\n client.getTrace({ traceId: id }).then((r) => setSpans(r.spans)).catch(console.error);\n }\n\n return (\n
\n
\n
\n Recent Traces\n
\n {traces.map((t) => (\n \n ))}\n {traces.length === 0 &&

No traces yet

}\n
\n
\n {selected\n ? <>

trace: {selected}

\n :

Select a trace to view the waterfall

\n }\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/components/TraceWaterfall.tsx dashboard/src/pages/Traces.tsx\ngit commit -m \"feat(dashboard): add trace waterfall viewer with typed Connect-ES\"\n```\n\n---\n\n## Task 10: Remaining Pages + Brain Config UI\n\n**Files:**\n- Create: `dashboard/src/pages/Nodes.tsx`\n- Create: `dashboard/src/components/BrainConfigPanel.tsx`\n- Create: `dashboard/src/pages/Metrics.tsx`\n- Create: `dashboard/src/pages/Issues.tsx`\n- Create: `dashboard/src/pages/Memory.tsx`\n- Create: `dashboard/src/pages/Convergence.tsx`\n\n- [ ] **Step 1: Write `BrainConfigPanel.tsx` — model switcher component**\n\n```tsx\nimport { useState, useEffect } from \"react\";\nimport { client } from \"../client\";\nimport type { NodeConfig, ProviderInfo } from \"../gen/omega/v1/types_pb\";\n\ninterface Props { nodeId: string }\n\nexport default function BrainConfigPanel({ nodeId }: Props) {\n const [config, setConfig] = useState(null);\n const [providers, setProviders] = useState([]);\n const [saving, setSaving] = useState(false);\n const [draft, setDraft] = useState({ provider: \"\", model: \"\", temperature: 0.7, maxTokens: 4096 });\n\n useEffect(() => {\n if (!nodeId) return;\n client.getNodeConfig({ nodeId }).then((r) => {\n setConfig(r.config ?? null);\n if (r.config?.brain) {\n const b = r.config.brain;\n setDraft({ provider: b.provider, model: b.model, temperature: b.temperature, maxTokens: b.maxTokens });\n }\n });\n client.listAvailableProviders({}).then((r) => setProviders(r.providers));\n }, [nodeId]);\n\n const currentProvider = providers.find((p) => p.id === draft.provider);\n\n async function save() {\n setSaving(true);\n try {\n const r = await client.updateNodeBrain({\n nodeId,\n brain: { provider: draft.provider, model: draft.model, temperature: draft.temperature, maxTokens: draft.maxTokens, systemPrompt: config?.brain?.systemPrompt ?? \"\", extraConfig: {} },\n });\n setConfig(r.config ?? null);\n } finally {\n setSaving(false);\n }\n }\n\n if (!config) return
Loading brain config…
;\n\n return (\n
\n

\n 🧠 LLM Brain Config\n

\n
\n
\n \n setDraft({ ...draft, provider: e.target.value, model: \"\" })}\n >\n {providers.map((p) => )}\n \n
\n
\n \n setDraft({ ...draft, model: e.target.value })}\n >\n {(currentProvider?.models ?? []).map((m) => )}\n \n
\n
\n \n setDraft({ ...draft, temperature: parseFloat(e.target.value) })}\n />\n
\n
\n \n setDraft({ ...draft, maxTokens: parseInt(e.target.value, 10) })}\n />\n
\n
\n \n {saving ? \"Saving…\" : \"Apply Brain Config\"}\n \n
\n );\n}\n```\n\n- [ ] **Step 2: Write stubs for remaining pages (iterate to full implementations)**\n\nEach page follows the same pattern: call `client.methodName({})`, display results. Write full implementations using the generated types.\n\n`Nodes.tsx` — calls `client.listNodes`, `client.getNode`, `client.getMetricsHistory`; includes `` in the node detail panel\n`Metrics.tsx` — calls `client.getMetricsHistory`, renders per-node `LineChart` with recharts\n`Issues.tsx` — calls `client.listIssues({ stateFilter: \"open\" })`, renders sortable table\n`Memory.tsx` — calls `client.getNodeMemory({ nodeId: \"\" })` for global view, semantic concept list\n`Convergence.tsx` — calls `client.getConvergence`, `client.listImprovements`, renders score chart\n\nThese pages follow the same structure as shown in Tasks 8 and 9. Write them using the generated proto types from `./gen/omega/v1/types_pb`.\n\n**`Nodes.tsx` detail panel pattern:**\n```tsx\n// When a node is selected, show node stats + brain config side by side\n{selectedNode && (\n
\n
\n {/* node stats, latency chart, execution history */}\n
\n \n
\n)}\n```\n\n- [ ] **Step 2: Verify build**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run build 2>&1 | tail -10\n```\n\nExpected: `built in Xs` with no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/\ngit commit -m \"feat(dashboard): complete all 7 pages\"\n```\n\n---\n\n## Task 11: End-to-End Verification\n\n- [ ] **Step 1: Run one Python heartbeat to populate SQLite DBs**\n\n```bash\ncd /Users/benebsworth/projects/omega\npython -m omega.examples.victoria_main --once\n```\n\nExpected: heartbeat runs, data written to `/tmp/omega_victoria_state.db`.\n\n- [ ] **Step 2: Start Go API server**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\n```\n\n- [ ] **Step 3: Verify key endpoints**\n\n```bash\n# Health\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Nodes\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool | head -30\n\n# Traces\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListTraces \\\n -H \"Content-Type: application/json\" -d '{\"limit\": 5}' | python3 -m json.tool\n\n# Brain config — list providers\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListAvailableProviders \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Brain config — get node config (replace NODE_ID with a real node_id from ListNodes)\nNODE_ID=$(curl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['nodes'][0]['nodeId'])\" 2>/dev/null)\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetNodeConfig \\\n -H \"Content-Type: application/json\" -d \"{\\\"nodeId\\\": \\\"$NODE_ID\\\"}\" | python3 -m json.tool\n```\n\nExpected: nodes with health scores, at least one trace, providers list with 5 entries, node config defaulting to `claude-sonnet-4-6`.\n\n- [ ] **Step 4: Start frontend**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev &\nsleep 2\necho \"Dashboard at http://localhost:5173\"\n```\n\n- [ ] **Step 5: Verify via Playwright MCP**\n\nUse `mcp__plugin_playwright_playwright__browser_navigate` to visit `http://localhost:5173` and take a screenshot to confirm the dashboard loads.\n\n- [ ] **Step 6: Final commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add -A\ngit commit -m \"feat: complete Omega Go+Connect-RPC API and React dashboard\"\n```\n\n---\n\n## Quick-start (reference)\n\n```bash\n# Terminal 1: Go API\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api && ./omega-api\n\n# Terminal 2: React dashboard\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev\n\n# Terminal 3: Python orchestrator\npython -m omega.examples.victoria_main --heartbeat 60\n\n# Open dashboard\nopen http://localhost:5173\n```\n"}, "6aacba45811078ea307cc43f195afd1820abcfc5": {"t": 1785764522.6597192, "n": 100004, "h": "0bdffc42d150ad01", "c": "# Omega Web Dashboard — Go + Connect-RPC + Protobuf Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Build a Go Connect-RPC API server + React/TypeScript dashboard with Connect-ES that reads from Omega's SQLite state store and renders live visibility into the self-improving agent system.\n\n**Architecture:** Proto definitions are the contract. Go server (`cmd/omega-api`) reads from the shared SQLite DBs that the Python node layer writes to. React dashboard uses `@connectrpc/connect-web` to call the Go server directly — no REST, no WebSockets. Real-time updates use server-streaming RPC (`StreamEvents`).\n\n**Tech Stack:** Go 1.24+, `connectrpc.com/connect`, `modernc.org/sqlite`, `google.golang.org/protobuf`, Buf CLI; Vite 5, React 18, TypeScript, `@connectrpc/connect-web` v2, `@bufbuild/protobuf`, Tailwind CSS, shadcn/ui, recharts\n\n---\n\n## Key Data Facts (from existing Python codebase)\n\n- **State store DB**: `/tmp/omega_victoria_state.db`\n - Tables: `nodes`, `node_executions`, `traces`, `cost_events`, `issues`, `activity_log`, `improvement_log`, `config_revisions`\n- **Memory DB**: `/tmp/omega_victoria_memory.db`\n - Tables: `episodes`, `semantic_memories`, `memory_ratings`\n- **Convergence data**: `episodes WHERE event_type='cycle_summary'` contains `score`, `pipeline_ms` per cycle\n- **Node namespacing**: memory is namespaced by node name (e.g., `\"DataIngestionNode\"`)\n- **Toolchain available**: Go 1.26, Buf 1.47, Node 22, npm 10\n\n---\n\n## File Map\n\n### Proto definitions\n| File | Responsibility |\n|------|---------------|\n| `buf.yaml` | Buf workspace config |\n| `buf.gen.yaml` | Code generation config (Go + TypeScript) |\n| `proto/omega/v1/omega_service.proto` | OrchestratorService — all dashboard RPCs |\n| `proto/omega/v1/node_service.proto` | NodeService — inter-node communication contract |\n| `proto/omega/v1/types.proto` | Shared message types (Node, Span, Issue, etc.) |\n\n### Go backend\n| File | Responsibility |\n|------|---------------|\n| `go.mod` | Go module definition |\n| `cmd/omega-api/main.go` | Server entry point, Connect handler registration |\n| `internal/db/db.go` | SQLite read layer — all queries |\n| `internal/handler/orchestrator.go` | OrchestratorService implementation |\n\n### Generated (do not hand-edit)\n| Path | Generated by |\n|------|-------------|\n| `gen/go/omega/v1/` | buf generate → Go protobuf + connect stubs |\n| `dashboard/src/gen/` | buf generate → TypeScript protobuf + connect clients |\n\n### Frontend\n| File | Responsibility |\n|------|---------------|\n| `dashboard/src/client.ts` | Connect transport + service client factory |\n| `dashboard/src/components/layout/Sidebar.tsx` | Nav sidebar |\n| `dashboard/src/components/layout/Header.tsx` | Status bar + orchestrator controls |\n| `dashboard/src/components/NodeCard.tsx` | Node health card with sparkline |\n| `dashboard/src/components/TraceWaterfall.tsx` | SVG waterfall for span trees |\n| `dashboard/src/components/BrainConfigPanel.tsx` | LLM model switcher panel (provider + model selectors) |\n| `dashboard/src/pages/Dashboard.tsx` | Overview page |\n| `dashboard/src/pages/Nodes.tsx` | Node table + detail + BrainConfigPanel |\n| `dashboard/src/pages/Traces.tsx` | Trace list + waterfall |\n| `dashboard/src/pages/Metrics.tsx` | Time-series charts |\n| `dashboard/src/pages/Issues.tsx` | Issues table |\n| `dashboard/src/pages/Memory.tsx` | Memory kernel visualization |\n| `dashboard/src/pages/Convergence.tsx` | Convergence + improvement log |\n| `dashboard/src/App.tsx` | Router + layout wrapper |\n\n---\n\n## Task 1: Proto Definitions\n\n**Files:**\n- Create: `buf.yaml`\n- Create: `buf.gen.yaml`\n- Create: `proto/omega/v1/types.proto`\n- Create: `proto/omega/v1/omega_service.proto`\n- Create: `proto/omega/v1/node_service.proto`\n\n- [ ] **Step 1: Write `buf.yaml`**\n\n```yaml\nversion: v2\nmodules:\n - path: proto\ndeps:\n - buf.build/googleapis/googleapis\nlint:\n use:\n - STANDARD\nbreaking:\n use:\n - FILE\n```\n\n- [ ] **Step 2: Write `buf.gen.yaml`**\n\n```yaml\nversion: v2\nmanaged:\n enabled: true\nplugins:\n - remote: buf.build/protocolbuffers/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/connectrpc/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/bufbuild/es\n out: dashboard/src/gen\n opt:\n - target=ts\n - remote: buf.build/connectrpc/es\n out: dashboard/src/gen\n opt:\n - target=ts\n```\n\n- [ ] **Step 3: Write `proto/omega/v1/types.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage Node {\n string node_id = 1;\n string name = 2;\n string version = 3;\n repeated string capabilities = 4;\n double health = 5;\n string status = 6;\n google.protobuf.Timestamp registered_at = 7;\n google.protobuf.Timestamp last_updated = 8;\n int64 executions_total = 9;\n double error_rate = 10;\n double avg_latency_ms = 11;\n double p95_latency_ms = 12;\n int64 improvement_count = 13;\n ExecutionRecord last_execution = 14;\n}\n\nmessage ExecutionRecord {\n string exec_id = 1;\n string node_id = 2;\n string node_name = 3;\n string trace_id = 4;\n string span_id = 5;\n string action = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n bool success = 10;\n string error_text = 11;\n map metrics = 12;\n int64 cycle = 13;\n}\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage TraceSummary {\n string trace_id = 1;\n google.protobuf.Timestamp trace_started = 2;\n google.protobuf.Timestamp trace_ended = 3;\n double total_duration_ms = 4;\n int64 span_count = 5;\n int64 error_spans = 6;\n int64 cycle = 7;\n}\n\nmessage Span {\n string span_id = 1;\n string trace_id = 2;\n string parent_span_id = 3;\n string node_id = 4;\n string node_name = 5;\n string operation = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n string status = 10;\n int64 cycle = 11;\n}\n\n// ── Issues ─────────────────────────────────────────────────────────────────\n\nmessage Issue {\n string issue_id = 1;\n string detector = 2;\n string severity = 3;\n string description = 4;\n string state = 5;\n google.protobuf.Timestamp opened_at = 6;\n google.protobuf.Timestamp resolved_at = 7;\n int64 cycle_opened = 8;\n int64 cycle_resolved = 9;\n}\n\n// ── Activity ───────────────────────────────────────────────────────────────\n\nmessage ActivityEntry {\n string log_id = 1;\n string action_type = 2;\n string entity_type = 3;\n string entity_id = 4;\n google.protobuf.Timestamp recorded_at = 5;\n int64 cycle = 6;\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────\n\nmessage ImprovementRecord {\n string improve_id = 1;\n string node_id = 2;\n string node_name = 3;\n string from_version = 4;\n string to_version = 5;\n string triggered_by = 6;\n google.protobuf.Timestamp recorded_at = 7;\n int64 cycle = 8;\n map before_metrics = 9;\n map after_metrics = 10;\n}\n\n// ── System health ──────────────────────────────────────────────────────────\n\nmessage SystemHealth {\n string status = 1;\n double composite_score = 2;\n double avg_node_health = 3;\n int64 node_count = 4;\n int64 open_issues = 5;\n int64 error_issues = 6;\n double uptime_seconds = 7;\n int64 total_cycles = 8;\n}\n\n// ── Memory ─────────────────────────────────────────────────────────────────\n\nmessage SemanticConcept {\n string concept = 1;\n string content = 2;\n double confidence = 3;\n int64 evidence_count = 4;\n repeated string tags = 5;\n}\n\nmessage EpisodeEntry {\n string episode_id = 1;\n string event_type = 2;\n google.protobuf.Timestamp timestamp = 3;\n int64 cycle = 4;\n double importance = 5;\n repeated string tags = 6;\n}\n\n// ── Convergence ────────────────────────────────────────────────────────────\n\nmessage ConvergencePoint {\n int64 cycle = 1;\n google.protobuf.Timestamp timestamp = 2;\n double score = 3;\n double pipeline_ms = 4;\n}\n\n// ── Costs ──────────────────────────────────────────────────────────────────\n\nmessage CostEntry {\n string provider = 1;\n string node_id = 2;\n int64 calls = 3;\n double total_ms = 4;\n double total_cost_usd = 5;\n}\n\n// ── Events (for streaming) ─────────────────────────────────────────────────\n\nmessage EventResponse {\n string type = 1;\n oneof payload {\n SystemHealth health_update = 2;\n Node node_update = 3;\n Issue new_issue = 4;\n ImprovementRecord improvement = 5;\n ActivityEntry activity = 6;\n }\n}\n\n// ── LLM Brain config ───────────────────────────────────────────────────────\n\nmessage BrainConfig {\n string provider = 1; // \"anthropic\", \"openai\", \"google\", \"deepseek\", \"ollama\", \"codex\"\n string model = 2; // e.g. \"claude-sonnet-4-6\", \"gpt-4o\", \"gemini-2.0-flash\"\n double temperature = 3;\n int32 max_tokens = 4;\n string system_prompt = 5;\n map extra_config = 6;\n}\n\nmessage NodeConfig {\n string node_id = 1;\n string node_type = 2;\n BrainConfig brain = 3;\n map parameters = 4;\n string version = 5;\n google.protobuf.Timestamp updated_at = 6;\n}\n\nmessage BrainExecutionEntry {\n string exec_id = 1;\n string node_id = 2;\n string provider = 3;\n string model = 4;\n int64 prompt_tokens = 5;\n int64 completion_tokens = 6;\n double latency_ms = 7;\n bool success = 8;\n string error_text = 9;\n google.protobuf.Timestamp executed_at = 10;\n int64 cycle = 11;\n}\n\nmessage ProviderInfo {\n string id = 1; // \"anthropic\"\n string display_name = 2; // \"Anthropic\"\n repeated string models = 3;\n bool requires_api_key = 4;\n bool available = 5;\n}\n```\n\n- [ ] **Step 4: Write `proto/omega/v1/omega_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"omega/v1/types.proto\";\n\nservice OrchestratorService {\n rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);\n rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);\n rpc GetNode(GetNodeRequest) returns (GetNodeResponse);\n rpc ListNodeExecutions(ListNodeExecutionsRequest) returns (ListNodeExecutionsResponse);\n rpc ListTraces(ListTracesRequest) returns (ListTracesResponse);\n rpc GetTrace(GetTraceRequest) returns (GetTraceResponse);\n rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);\n rpc GetMetricsHistory(GetMetricsHistoryRequest) returns (GetMetricsHistoryResponse);\n rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);\n rpc ListActivity(ListActivityRequest) returns (ListActivityResponse);\n rpc ListImprovements(ListImprovementsRequest) returns (ListImprovementsResponse);\n rpc GetNodeMemory(GetNodeMemoryRequest) returns (GetNodeMemoryResponse);\n rpc GetConvergence(GetConvergenceRequest) returns (GetConvergenceResponse);\n rpc GetCosts(GetCostsRequest) returns (GetCostsResponse);\n rpc SubmitFeedback(SubmitFeedbackRequest) returns (SubmitFeedbackResponse);\n rpc StartOrchestrator(StartOrchestratorRequest) returns (StartOrchestratorResponse);\n rpc StopOrchestrator(StopOrchestratorRequest) returns (StopOrchestratorResponse);\n rpc TriggerHeartbeat(TriggerHeartbeatRequest) returns (TriggerHeartbeatResponse);\n rpc StreamEvents(StreamEventsRequest) returns (stream EventResponse);\n\n // ── Brain / LLM config ───────────────────────────────────────────────────\n rpc GetNodeConfig(GetNodeConfigRequest) returns (GetNodeConfigResponse);\n rpc UpdateNodeBrain(UpdateNodeBrainRequest) returns (UpdateNodeBrainResponse);\n rpc ListAvailableProviders(ListAvailableProvidersRequest) returns (ListAvailableProvidersResponse);\n rpc GetBrainExecutionHistory(GetBrainExecutionHistoryRequest) returns (GetBrainExecutionHistoryResponse);\n}\n\n// ── Health ─────────────────────────────────────────────────────────────────\n\nmessage GetHealthRequest {}\nmessage GetHealthResponse { SystemHealth health = 1; }\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage ListNodesRequest {}\nmessage ListNodesResponse { repeated Node nodes = 1; }\n\nmessage GetNodeRequest { string node_id = 1; }\nmessage GetNodeResponse {\n Node node = 1;\n repeated ExecutionRecord recent_executions = 2;\n repeated ImprovementRecord improvements = 3;\n repeated LatencyPoint latency_history = 4;\n}\n\nmessage LatencyPoint {\n google.protobuf.Timestamp ts = 1;\n double duration_ms = 2;\n bool success = 3;\n}\n\nmessage ListNodeExecutionsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListNodeExecutionsResponse { repeated ExecutionRecord executions = 1; }\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage ListTracesRequest { int32 limit = 1; }\nmessage ListTracesResponse { repeated TraceSummary traces = 1; }\n\nmessage GetTraceRequest { string trace_id = 1; }\nmessage GetTraceResponse { repeated Span spans = 1; }\n\n// ── Metrics ─────────────────────────────────────────────────────────────────\n\nmessage GetMetricsRequest {}\nmessage GetMetricsResponse {\n SystemHealth system = 1;\n repeated Node nodes = 2;\n repeated CostEntry costs = 3;\n repeated Issue open_issues = 4;\n repeated TraceSummary recent_traces = 5;\n}\n\nmessage GetMetricsHistoryRequest { string node_id = 1; }\nmessage GetMetricsHistoryResponse {\n repeated NodeLatencyHistory node_histories = 1;\n}\nmessage NodeLatencyHistory {\n string node_id = 1;\n string name = 2;\n repeated LatencyPoint history = 3;\n}\n\n// ── Issues ──────────────────────────────────────────────────────────────────\n\nmessage ListIssuesRequest { string state_filter = 1; }\nmessage ListIssuesResponse { repeated Issue issues = 1; }\n\n// ── Activity ─────────────────────────────────────────────────────────────────\n\nmessage ListActivityRequest { int32 limit = 1; }\nmessage ListActivityResponse { repeated ActivityEntry entries = 1; }\n\n// ── Improvements ─────────────────────────────────────────────────────────────\n\nmessage ListImprovementsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListImprovementsResponse { repeated ImprovementRecord improvements = 1; }\n\n// ── Memory ───────────────────────────────────────────────────────────────────\n\nmessage GetNodeMemoryRequest { string node_id = 1; }\nmessage GetNodeMemoryResponse {\n string namespace = 1;\n int64 episodic_count = 2;\n int64 semantic_count = 3;\n repeated SemanticConcept top_semantic = 4;\n repeated EpisodeEntry recent_episodes = 5;\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nmessage GetConvergenceRequest { int32 limit = 1; }\nmessage GetConvergenceResponse { repeated ConvergencePoint points = 1; }\n\n// ── Costs ────────────────────────────────────────────────────────────────────\n\nmessage GetCostsRequest {}\nmessage GetCostsResponse { repeated CostEntry costs = 1; }\n\n// ── Feedback ─────────────────────────────────────────────────────────────────\n\nmessage SubmitFeedbackRequest {\n string text = 1;\n string target_node = 2;\n}\nmessage SubmitFeedbackResponse { bool ok = 1; }\n\n// ── Orchestrator control ─────────────────────────────────────────────────────\n\nmessage StartOrchestratorRequest { int32 heartbeat_secs = 1; }\nmessage StartOrchestratorResponse { bool started = 1; string message = 2; }\n\nmessage StopOrchestratorRequest {}\nmessage StopOrchestratorResponse { bool stopped = 1; string message = 2; }\n\nmessage TriggerHeartbeatRequest {}\nmessage TriggerHeartbeatResponse { bool triggered = 1; string message = 2; }\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nmessage StreamEventsRequest { int32 poll_interval_ms = 1; }\n\n// ── Brain config ──────────────────────────────────────────────────────────────\n\nmessage GetNodeConfigRequest { string node_id = 1; }\nmessage GetNodeConfigResponse { NodeConfig config = 1; }\n\nmessage UpdateNodeBrainRequest {\n string node_id = 1;\n BrainConfig brain = 2;\n}\nmessage UpdateNodeBrainResponse { NodeConfig config = 1; }\n\nmessage ListAvailableProvidersRequest {}\nmessage ListAvailableProvidersResponse { repeated ProviderInfo providers = 1; }\n\nmessage GetBrainExecutionHistoryRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage GetBrainExecutionHistoryResponse { repeated BrainExecutionEntry entries = 1; }\n```\n\n- [ ] **Step 5: Write `proto/omega/v1/node_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\n// NodeService — contract for inter-node communication.\n// Python nodes implement this; Go orchestrator calls it.\nservice NodeService {\n rpc Execute(ExecuteRequest) returns (ExecuteResponse);\n rpc Evaluate(EvaluateRequest) returns (EvaluateResponse);\n rpc Improve(ImproveRequest) returns (ImproveResponse);\n rpc GetState(GetStateRequest) returns (GetStateResponse);\n rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);\n}\n\nmessage ExecuteRequest {\n string action = 1;\n map parameters = 2;\n map context = 3;\n}\n\nmessage ExecuteResponse {\n bool success = 1;\n bytes result_json = 2;\n repeated string errors = 3;\n map metrics = 4;\n}\n\nmessage EvaluateRequest {}\nmessage EvaluateResponse { map metrics = 1; }\n\nmessage ImproveRequest { map feedback = 1; }\nmessage ImproveResponse { bool changed = 1; string new_version = 2; }\n\nmessage GetStateRequest {}\nmessage GetStateResponse {\n string node_id = 1;\n string name = 2;\n string version = 3;\n double health = 4;\n string status = 5;\n repeated string capabilities = 6;\n}\n\nmessage GetCapabilitiesRequest {}\nmessage GetCapabilitiesResponse { repeated string capabilities = 1; }\n```\n\n- [ ] **Step 6: Lint and validate protos**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf dep update\nbuf lint\n```\n\nExpected: no errors.\n\n- [ ] **Step 7: Commit protos and buf config**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add buf.yaml buf.gen.yaml proto/\ngit commit -m \"feat(proto): add Omega protobuf service definitions\"\n```\n\n---\n\n## Task 2: Go Module + Code Generation\n\n**Files:**\n- Create: `go.mod`\n- Create: `go.sum` (generated)\n- Create: `gen/go/` (generated)\n- Create: `dashboard/src/gen/` (generated)\n\n- [ ] **Step 1: Initialize Go module**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo mod init github.com/benebsworth/omega\n```\n\n- [ ] **Step 2: Add Go dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo get connectrpc.com/connect@latest\ngo get google.golang.org/protobuf@latest\ngo get modernc.org/sqlite@latest\ngo get golang.org/x/net@latest\n```\n\n- [ ] **Step 3: Run buf generate**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf generate\n```\n\nExpected: creates `gen/go/omega/v1/*.pb.go` and `gen/go/omega/v1/*connect*.go`, and `dashboard/src/gen/` with TypeScript files.\n\n- [ ] **Step 4: Verify generated Go compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./gen/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 5: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add go.mod go.sum gen/ dashboard/src/gen/\ngit commit -m \"feat(gen): add Go module and run buf generate for Go + TS stubs\"\n```\n\n---\n\n## Task 3: Go Database Layer\n\n**Files:**\n- Create: `internal/db/db.go`\n\n- [ ] **Step 1: Write `internal/db/db.go`**\n\n```go\n// Package db provides read-only access to the Omega SQLite state and memory databases.\n// The Python node layer writes; this Go layer reads.\npackage db\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t_ \"modernc.org/sqlite\"\n)\n\nconst (\n\tStateDBPath = \"/tmp/omega_victoria_state.db\"\n\tMemoryDBPath = \"/tmp/omega_victoria_memory.db\"\n)\n\n// DB holds connections to both Omega SQLite databases.\ntype DB struct {\n\tstate *sql.DB\n\tmemory *sql.DB\n}\n\n// New opens both databases in WAL mode for concurrent reads.\nfunc New(stateDBPath, memoryDBPath string) (*DB, error) {\n\tstate, err := openDB(stateDBPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open state db: %w\", err)\n\t}\n\tmemory, err := openDB(memoryDBPath)\n\tif err != nil {\n\t\tstate.Close()\n\t\treturn nil, fmt.Errorf(\"open memory db: %w\", err)\n\t}\n\td := &DB{state: state, memory: memory}\n\tif err := d.ensureBrainTables(); err != nil {\n\t\tstate.Close()\n\t\tmemory.Close()\n\t\treturn nil, fmt.Errorf(\"ensure brain tables: %w\", err)\n\t}\n\treturn d, nil\n}\n\nfunc openDB(path string) (*sql.DB, error) {\n\tdb, err := sql.Open(\"sqlite\", path+\"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdb.SetMaxOpenConns(1) // SQLite WAL: one writer, many readers via connection pooling\n\tdb.SetMaxIdleConns(1)\n\treturn db, db.Ping()\n}\n\nfunc (d *DB) Close() {\n\td.state.Close()\n\td.memory.Close()\n}\n\n// ── Node types ──────────────────────────────────────────────────────────────\n\ntype Node struct {\n\tNodeID string\n\tName string\n\tVersion string\n\tCapabilities []string\n\tHealth float64\n\tStatus string\n\tRegisteredAt float64\n\tLastUpdated float64\n\tExecutionsTotal int64\n\tErrorRate float64\n\tAvgLatencyMS float64\n\tP95LatencyMS float64\n\tImprovementCount int64\n\tLastExecution *Execution\n}\n\ntype Execution struct {\n\tExecID string\n\tNodeID string\n\tNodeName string\n\tTraceID string\n\tSpanID string\n\tAction string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tSuccess bool\n\tErrorText string\n\tMetrics map[string]float64\n\tCycle int64\n}\n\ntype LatencyPoint struct {\n\tStartedAt float64\n\tDurationMS float64\n\tSuccess bool\n}\n\n// ── Trace types ─────────────────────────────────────────────────────────────\n\ntype TraceSummary struct {\n\tTraceID string\n\tTraceStarted float64\n\tTraceEnded *float64\n\tTotalDurationMS float64\n\tSpanCount int64\n\tErrorSpans int64\n\tCycle int64\n}\n\ntype Span struct {\n\tSpanID string\n\tTraceID string\n\tParentSpanID string\n\tNodeID string\n\tNodeName string\n\tOperation string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tStatus string\n\tCycle int64\n}\n\n// ── Issue types ──────────────────────────────────────────────────────────────\n\ntype Issue struct {\n\tIssueID string\n\tDetector string\n\tSeverity string\n\tDescription string\n\tState string\n\tOpenedAt float64\n\tResolvedAt *float64\n\tCycleOpened int64\n\tCycleResolved *int64\n}\n\n// ── Activity types ────────────────────────────────────────────────────────────\n\ntype ActivityEntry struct {\n\tLogID string\n\tActionType string\n\tEntityType string\n\tEntityID string\n\tRecordedAt float64\n\tCycle int64\n}\n\n// ── Improvement types ─────────────────────────────────────────────────────────\n\ntype Improvement struct {\n\tImproveID string\n\tNodeID string\n\tNodeName string\n\tFromVersion string\n\tToVersion string\n\tTriggeredBy string\n\tRecordedAt float64\n\tCycle int64\n\tBeforeMetrics map[string]float64\n\tAfterMetrics map[string]float64\n}\n\n// ── Cost types ────────────────────────────────────────────────────────────────\n\ntype CostEntry struct {\n\tProvider string\n\tNodeID string\n\tCalls int64\n\tTotalMS float64\n\tTotalCostUSD float64\n}\n\n// ── Memory types ──────────────────────────────────────────────────────────────\n\ntype SemanticConcept struct {\n\tConcept string\n\tContent string\n\tConfidence float64\n\tEvidenceCount int64\n\tTags []string\n}\n\ntype EpisodeEntry struct {\n\tEpisodeID string\n\tEventType string\n\tTimestamp float64\n\tCycle int64\n\tImportance float64\n\tTags []string\n}\n\ntype ConvergencePoint struct {\n\tCycle int64\n\tTimestamp float64\n\tScore float64\n\tPipelineMS float64\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\ntype SystemHealth struct {\n\tStatus string\n\tCompositeScore float64\n\tAvgNodeHealth float64\n\tNodeCount int64\n\tOpenIssues int64\n\tErrorIssues int64\n\tUptimeSeconds float64\n\tTotalCycles int64\n}\n\n// ── Query methods ──────────────────────────────────────────────────────────────\n\n// AllNodes returns all registered nodes with computed metrics.\nfunc (d *DB) AllNodes() ([]*Node, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes ORDER BY registered_at`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar nodes []*Node\n\tfor rows.Next() {\n\t\tn := &Node{}\n\t\tvar capsJSON string\n\t\tif err := rows.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\t\tnodes = append(nodes, n)\n\t}\n\n\t// Enrich with computed metrics\n\tfor _, n := range nodes {\n\t\tif err := d.enrichNode(n); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn nodes, nil\n}\n\n// GetNode returns one node by ID, enriched with metrics.\nfunc (d *DB) GetNode(nodeID string) (*Node, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes WHERE node_id = ?`, nodeID)\n\tn := &Node{}\n\tvar capsJSON string\n\tif err := row.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\treturn n, d.enrichNode(n)\n}\n\nfunc (d *DB) enrichNode(n *Node) error {\n\t// Execution stats\n\trow := d.state.QueryRow(`\n\t\tSELECT COUNT(*),\n\t\t SUM(CASE WHEN success=0 THEN 1 ELSE 0 END),\n\t\t AVG(duration_ms),\n\t\t MAX(started_at)\n\t\tFROM node_executions WHERE node_id = ?`, n.NodeID)\n\tvar total, failed int64\n\tvar avgMS sql.NullFloat64\n\tvar lastStarted sql.NullFloat64\n\trow.Scan(&total, &failed, &avgMS, &lastStarted) //nolint:errcheck\n\n\tn.ExecutionsTotal = total\n\tif total > 0 {\n\t\tn.ErrorRate = float64(failed) / float64(total)\n\t}\n\tif avgMS.Valid {\n\t\tn.AvgLatencyMS = avgMS.Float64\n\t}\n\n\t// p95 latency — two-query approach (SQLite rejects COUNT(*) inside OFFSET)\n\tvar p95Count int64\n\td.state.QueryRow(`SELECT COUNT(*) FROM node_executions WHERE node_id = ? AND duration_ms IS NOT NULL`, n.NodeID).Scan(&p95Count) //nolint:errcheck\n\tif p95Count > 0 {\n\t\toffset := int64(float64(p95Count) * 0.95)\n\t\tp95row := d.state.QueryRow(`\n\t\t\tSELECT duration_ms FROM node_executions\n\t\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\t\tORDER BY duration_ms\n\t\t\tLIMIT 1 OFFSET ?\n\t\t`, n.NodeID, offset)\n\t\tvar p95 sql.NullFloat64\n\t\tp95row.Scan(&p95) //nolint:errcheck\n\t\tif p95.Valid {\n\t\t\tn.P95LatencyMS = p95.Float64\n\t\t}\n\t}\n\n\t// Improvement count\n\trow2 := d.state.QueryRow(\n\t\t`SELECT COUNT(*) FROM improvement_log WHERE node_id = ?`, n.NodeID)\n\trow2.Scan(&n.ImprovementCount) //nolint:errcheck\n\n\t// Last execution\n\tlastExec, err := d.lastExecution(n.NodeID)\n\tif err == nil {\n\t\tn.LastExecution = lastExec\n\t}\n\treturn nil\n}\n\nfunc (d *DB) lastExecution(nodeID string) (*Execution, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\t action, started_at, ended_at, duration_ms, success,\n\t\t COALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE node_id = ?\n\t\tORDER BY started_at DESC LIMIT 1`, nodeID)\n\treturn scanExecution(row)\n}\n\nfunc scanExecution(row *sql.Row) (*Execution, error) {\n\te := &Execution{}\n\tvar metricsJSON string\n\tvar success int\n\tvar endedAt, durationMS sql.NullFloat64\n\terr := row.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t&e.ErrorText, &metricsJSON, &e.Cycle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\te.Success = success == 1\n\tif endedAt.Valid {\n\t\tv := endedAt.Float64\n\t\te.EndedAt = &v\n\t}\n\tif durationMS.Valid {\n\t\tv := durationMS.Float64\n\t\te.DurationMS = &v\n\t}\n\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\treturn e, nil\n}\n\n// GetExecutions returns recent executions, optionally filtered by node.\nfunc (d *DB) GetExecutions(nodeID string, limit int) ([]*Execution, error) {\n\tquery := `SELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\taction, started_at, ended_at, duration_ms, success,\n\t\tCOALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY started_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\treturn scanExecutions(rows)\n}\n\nfunc scanExecutions(rows *sql.Rows) ([]*Execution, error) {\n\tvar result []*Execution\n\tfor rows.Next() {\n\t\te := &Execution{}\n\t\tvar metricsJSON string\n\t\tvar success int\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t\t&e.ErrorText, &metricsJSON, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\te.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\te.DurationMS = &v\n\t\t}\n\t\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\t\tresult = append(result, e)\n\t}\n\treturn result, nil\n}\n\n// LatencyHistory returns (started_at, duration_ms, success) for charting.\nfunc (d *DB) LatencyHistory(nodeID string, limit int) ([]*LatencyPoint, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT started_at, duration_ms, success\n\t\tFROM node_executions\n\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\tORDER BY started_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*LatencyPoint\n\tfor rows.Next() {\n\t\tp := &LatencyPoint{}\n\t\tvar success int\n\t\tif err := rows.Scan(&p.StartedAt, &p.DurationMS, &success); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.Success = success == 1\n\t\tpoints = append(points, p)\n\t}\n\t// Reverse to chronological order\n\tfor i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {\n\t\tpoints[i], points[j] = points[j], points[i]\n\t}\n\treturn points, nil\n}\n\n// RecentTraces returns recent trace summaries.\nfunc (d *DB) RecentTraces(limit int) ([]*TraceSummary, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT trace_id,\n\t\t MIN(started_at) as trace_started,\n\t\t MAX(ended_at) as trace_ended,\n\t\t (MAX(ended_at) - MIN(started_at)) * 1000 as total_duration_ms,\n\t\t COUNT(*) as span_count,\n\t\t SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_spans,\n\t\t MAX(cycle) as cycle\n\t\tFROM traces\n\t\tGROUP BY trace_id\n\t\tORDER BY trace_started DESC\n\t\tLIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar summaries []*TraceSummary\n\tfor rows.Next() {\n\t\tt := &TraceSummary{}\n\t\tvar traceEnded sql.NullFloat64\n\t\tif err := rows.Scan(&t.TraceID, &t.TraceStarted, &traceEnded,\n\t\t\t&t.TotalDurationMS, &t.SpanCount, &t.ErrorSpans, &t.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif traceEnded.Valid {\n\t\t\tv := traceEnded.Float64\n\t\t\tt.TraceEnded = &v\n\t\t}\n\t\tsummaries = append(summaries, t)\n\t}\n\treturn summaries, nil\n}\n\n// GetTraceSpans returns all spans for a trace.\nfunc (d *DB) GetTraceSpans(traceID string) ([]*Span, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT span_id, trace_id, COALESCE(parent_span_id,''),\n\t\t COALESCE(node_id,''), COALESCE(node_name,''),\n\t\t operation, started_at, ended_at, duration_ms, status, cycle\n\t\tFROM traces WHERE trace_id = ? ORDER BY started_at`, traceID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar spans []*Span\n\tfor rows.Next() {\n\t\ts := &Span{}\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&s.SpanID, &s.TraceID, &s.ParentSpanID,\n\t\t\t&s.NodeID, &s.NodeName, &s.Operation,\n\t\t\t&s.StartedAt, &endedAt, &durationMS, &s.Status, &s.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\ts.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\ts.DurationMS = &v\n\t\t}\n\t\tspans = append(spans, s)\n\t}\n\treturn spans, nil\n}\n\n// GetIssues returns issues, optionally filtered by state.\nfunc (d *DB) GetIssues(stateFilter string) ([]*Issue, error) {\n\tquery := `SELECT issue_id, detector, severity, description, state,\n\t\topened_at, resolved_at, cycle_opened, cycle_resolved\n\t\tFROM issues WHERE 1=1`\n\targs := []any{}\n\tif stateFilter != \"\" && stateFilter != \"all\" {\n\t\tif stateFilter == \"open\" {\n\t\t\tquery += \" AND state != 'resolved'\"\n\t\t} else {\n\t\t\tquery += \" AND state = ?\"\n\t\t\targs = append(args, stateFilter)\n\t\t}\n\t}\n\tquery += \" ORDER BY opened_at DESC LIMIT 100\"\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar issues []*Issue\n\tfor rows.Next() {\n\t\ti := &Issue{}\n\t\tif err := rows.Scan(&i.IssueID, &i.Detector, &i.Severity, &i.Description,\n\t\t\t&i.State, &i.OpenedAt, &i.ResolvedAt, &i.CycleOpened, &i.CycleResolved); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tissues = append(issues, i)\n\t}\n\treturn issues, nil\n}\n\n// RecentActivity returns recent activity log entries.\nfunc (d *DB) RecentActivity(limit int) ([]*ActivityEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT log_id, action_type, entity_type, entity_id, recorded_at, cycle\n\t\tFROM activity_log ORDER BY recorded_at DESC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*ActivityEntry\n\tfor rows.Next() {\n\t\te := &ActivityEntry{}\n\t\tif err := rows.Scan(&e.LogID, &e.ActionType, &e.EntityType,\n\t\t\t&e.EntityID, &e.RecordedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n\n// GetImprovements returns improvement log, optionally by node.\nfunc (d *DB) GetImprovements(nodeID string, limit int) ([]*Improvement, error) {\n\tquery := `SELECT improve_id, node_id, node_name, from_version, to_version,\n\t\ttriggered_by, recorded_at, cycle, before_metrics_json, after_metrics_json\n\t\tFROM improvement_log WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY recorded_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar imps []*Improvement\n\tfor rows.Next() {\n\t\timp := &Improvement{}\n\t\tvar beforeJSON, afterJSON string\n\t\tif err := rows.Scan(&imp.ImproveID, &imp.NodeID, &imp.NodeName,\n\t\t\t&imp.FromVersion, &imp.ToVersion, &imp.TriggeredBy,\n\t\t\t&imp.RecordedAt, &imp.Cycle, &beforeJSON, &afterJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(beforeJSON), &imp.BeforeMetrics) //nolint:errcheck\n\t\tjson.Unmarshal([]byte(afterJSON), &imp.AfterMetrics) //nolint:errcheck\n\t\timps = append(imps, imp)\n\t}\n\treturn imps, nil\n}\n\n// GetCosts returns aggregated cost by provider and node.\nfunc (d *DB) GetCosts() ([]*CostEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT provider, node_id,\n\t\t COUNT(*) as calls,\n\t\t SUM(duration_ms) as total_ms,\n\t\t SUM(estimated_cost_usd) as total_cost\n\t\tFROM cost_events GROUP BY provider, node_id`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar costs []*CostEntry\n\tfor rows.Next() {\n\t\tc := &CostEntry{}\n\t\tif err := rows.Scan(&c.Provider, &c.NodeID, &c.Calls, &c.TotalMS, &c.TotalCostUSD); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcosts = append(costs, c)\n\t}\n\treturn costs, nil\n}\n\n// GetConvergence reads cycle_summary episodes from the memory DB.\nfunc (d *DB) GetConvergence(limit int) ([]*ConvergencePoint, error) {\n\trows, err := d.memory.Query(`\n\t\tSELECT cycle, timestamp, content_json\n\t\tFROM episodes WHERE event_type = 'cycle_summary'\n\t\tORDER BY cycle ASC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*ConvergencePoint\n\tfor rows.Next() {\n\t\tp := &ConvergencePoint{}\n\t\tvar contentJSON string\n\t\tif err := rows.Scan(&p.Cycle, &p.Timestamp, &contentJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar content map[string]any\n\t\tif err := json.Unmarshal([]byte(contentJSON), &content); err == nil {\n\t\t\tif s, ok := content[\"score\"].(float64); ok {\n\t\t\t\tp.Score = s\n\t\t\t}\n\t\t\tif ms, ok := content[\"pipeline_ms\"].(float64); ok {\n\t\t\t\tp.PipelineMS = ms\n\t\t\t}\n\t\t}\n\t\tpoints = append(points, p)\n\t}\n\treturn points, nil\n}\n\n// GetMemoryStats returns episodic + semantic counts for a namespace.\nfunc (d *DB) GetMemoryStats(namespace string) (int64, int64, []*SemanticConcept, []*EpisodeEntry, error) {\n\tvar epCount, semCount int64\n\n\t// Episodic count\n\tquery := \"SELECT COUNT(*) FROM episodes\"\n\targs := []any{}\n\tif namespace != \"\" {\n\t\tquery += \" WHERE namespace = ?\"\n\t\targs = append(args, namespace)\n\t}\n\td.memory.QueryRow(query, args...).Scan(&epCount) //nolint:errcheck\n\n\t// Semantic count\n\tsemQuery := \"SELECT COUNT(*) FROM semantic_memories\"\n\tsemArgs := []any{}\n\tif namespace != \"\" {\n\t\tsemQuery += \" WHERE namespace = ?\"\n\t\tsemArgs = append(semArgs, namespace)\n\t}\n\td.memory.QueryRow(semQuery, semArgs...).Scan(&semCount) //nolint:errcheck\n\n\t// Top semantic\n\tsemRowQuery := \"SELECT concept, content, confidence, evidence_count, tags_json FROM semantic_memories\"\n\tif namespace != \"\" {\n\t\tsemRowQuery += \" WHERE namespace = ?\"\n\t}\n\tsemRowQuery += \" ORDER BY confidence DESC LIMIT 20\"\n\tsemRows, err := d.memory.Query(semRowQuery, semArgs...)\n\tif err != nil {\n\t\treturn epCount, semCount, nil, nil, err\n\t}\n\tdefer semRows.Close()\n\tvar concepts []*SemanticConcept\n\tfor semRows.Next() {\n\t\tc := &SemanticConcept{}\n\t\tvar tagsJSON string\n\t\tif err := semRows.Scan(&c.Concept, &c.Content, &c.Confidence, &c.EvidenceCount, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, nil, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &c.Tags) //nolint:errcheck\n\t\tconcepts = append(concepts, c)\n\t}\n\n\t// Recent episodes\n\tepRowQuery := \"SELECT episode_id, event_type, timestamp, cycle, importance, tags_json FROM episodes\"\n\tif namespace != \"\" {\n\t\tepRowQuery += \" WHERE namespace = ?\"\n\t}\n\tepRowQuery += \" ORDER BY timestamp DESC LIMIT 20\"\n\tepRows, err := d.memory.Query(epRowQuery, args...)\n\tif err != nil {\n\t\treturn epCount, semCount, concepts, nil, err\n\t}\n\tdefer epRows.Close()\n\tvar episodes []*EpisodeEntry\n\tfor epRows.Next() {\n\t\te := &EpisodeEntry{}\n\t\tvar tagsJSON string\n\t\tif err := epRows.Scan(&e.EpisodeID, &e.EventType, &e.Timestamp, &e.Cycle, &e.Importance, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, concepts, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &e.Tags) //nolint:errcheck\n\t\tepisodes = append(episodes, e)\n\t}\n\n\treturn epCount, semCount, concepts, episodes, nil\n}\n\n// SystemHealth computes overall system health from node states.\nfunc (d *DB) SystemHealth() (*SystemHealth, error) {\n\tnodes, err := d.AllNodes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn &SystemHealth{Status: \"no_nodes\"}, nil\n\t}\n\n\tvar totalHealth float64\n\tvar minHealth float64 = 1.0\n\tfor _, n := range nodes {\n\t\ttotalHealth += n.Health\n\t\tif n.Health < minHealth {\n\t\t\tminHealth = n.Health\n\t\t}\n\t}\n\tavgHealth := totalHealth / float64(len(nodes))\n\n\tissues, _ := d.GetIssues(\"open\")\n\topenCount := int64(len(issues))\n\tvar errorIssues int64\n\tfor _, i := range issues {\n\t\tif i.Severity == \"error\" {\n\t\t\terrorIssues++\n\t\t}\n\t}\n\n\tissuePenalty := float64(errorIssues) * 0.1\n\tif issuePenalty > 0.3 {\n\t\tissuePenalty = 0.3\n\t}\n\tcomposite := avgHealth - issuePenalty\n\tif composite < 0 {\n\t\tcomposite = 0\n\t}\n\n\tstatus := \"healthy\"\n\tif composite < 0.6 {\n\t\tstatus = \"critical\"\n\t} else if composite < 0.8 {\n\t\tstatus = \"degraded\"\n\t}\n\n\t// Uptime from first registered_at\n\tvar minRegAt float64 = float64(time.Now().Unix())\n\tfor _, n := range nodes {\n\t\tif n.RegisteredAt < minRegAt {\n\t\t\tminRegAt = n.RegisteredAt\n\t\t}\n\t}\n\tuptime := float64(time.Now().Unix()) - minRegAt\n\n\t// Total cycles\n\tvar totalCycles int64\n\td.state.QueryRow(\"SELECT COALESCE(MAX(cycle), 0) FROM node_executions\").Scan(&totalCycles) //nolint:errcheck\n\n\treturn &SystemHealth{\n\t\tStatus: status,\n\t\tCompositeScore: round3(composite),\n\t\tAvgNodeHealth: round3(avgHealth),\n\t\tNodeCount: int64(len(nodes)),\n\t\tOpenIssues: openCount,\n\t\tErrorIssues: errorIssues,\n\t\tUptimeSeconds: uptime,\n\t\tTotalCycles: totalCycles,\n\t}, nil\n}\n\nfunc round3(f float64) float64 {\n\treturn float64(int(f*1000+0.5)) / 1000\n}\n\n// ── Brain config types ─────────────────────────────────────────────────────────\n\n// BrainConfig is the LLM adapter config for a node — stored in Go-managed tables.\ntype BrainConfig struct {\n\tNodeID string\n\tProvider string\n\tModel string\n\tTemperature float64\n\tMaxTokens int64\n\tSystemPrompt string\n\tExtraConfig map[string]string\n\tUpdatedAt float64\n}\n\ntype BrainExecutionEntry struct {\n\tExecID string\n\tNodeID string\n\tProvider string\n\tModel string\n\tPromptTokens int64\n\tCompletionTokens int64\n\tLatencyMS float64\n\tSuccess bool\n\tErrorText string\n\tExecutedAt float64\n\tCycle int64\n}\n\n// ── Brain config DB methods ────────────────────────────────────────────────────\n\n// ensureBrainTables creates the Go-managed brain config tables if they don't exist.\n// Call this in New() before returning.\nfunc (d *DB) ensureBrainTables() error {\n\t_, err := d.state.Exec(`\n\t\tCREATE TABLE IF NOT EXISTS node_brain_config (\n\t\t\tnode_id TEXT PRIMARY KEY,\n\t\t\tprovider TEXT NOT NULL DEFAULT 'anthropic',\n\t\t\tmodel TEXT NOT NULL DEFAULT 'claude-sonnet-4-6',\n\t\t\ttemperature REAL NOT NULL DEFAULT 0.7,\n\t\t\tmax_tokens INTEGER NOT NULL DEFAULT 4096,\n\t\t\tsystem_prompt TEXT NOT NULL DEFAULT '',\n\t\t\textra_config TEXT NOT NULL DEFAULT '{}',\n\t\t\tupdated_at REAL NOT NULL DEFAULT (unixepoch('now'))\n\t\t);\n\t\tCREATE TABLE IF NOT EXISTS brain_execution_log (\n\t\t\texec_id TEXT PRIMARY KEY,\n\t\t\tnode_id TEXT NOT NULL,\n\t\t\tprovider TEXT NOT NULL,\n\t\t\tmodel TEXT NOT NULL,\n\t\t\tprompt_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tcompletion_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tlatency_ms REAL NOT NULL DEFAULT 0,\n\t\t\tsuccess INTEGER NOT NULL DEFAULT 1,\n\t\t\terror_text TEXT NOT NULL DEFAULT '',\n\t\t\texecuted_at REAL NOT NULL DEFAULT (unixepoch('now')),\n\t\t\tcycle INTEGER NOT NULL DEFAULT 0\n\t\t);`)\n\treturn err\n}\n\n// GetBrainConfig returns the brain config for a node (defaults if not set).\nfunc (d *DB) GetBrainConfig(nodeID string) (*BrainConfig, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, provider, model, temperature, max_tokens,\n\t\t system_prompt, extra_config, updated_at\n\t\tFROM node_brain_config WHERE node_id = ?`, nodeID)\n\tc := &BrainConfig{}\n\tvar extraJSON string\n\terr := row.Scan(&c.NodeID, &c.Provider, &c.Model, &c.Temperature,\n\t\t&c.MaxTokens, &c.SystemPrompt, &extraJSON, &c.UpdatedAt)\n\tif err == sql.ErrNoRows {\n\t\t// Return defaults\n\t\treturn &BrainConfig{\n\t\t\tNodeID: nodeID,\n\t\t\tProvider: \"anthropic\",\n\t\t\tModel: \"claude-sonnet-4-6\",\n\t\t\tTemperature: 0.7,\n\t\t\tMaxTokens: 4096,\n\t\t}, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(extraJSON), &c.ExtraConfig) //nolint:errcheck\n\treturn c, nil\n}\n\n// SetBrainConfig upserts the brain config for a node.\nfunc (d *DB) SetBrainConfig(c *BrainConfig) error {\n\textraJSON, _ := json.Marshal(c.ExtraConfig)\n\t_, err := d.state.Exec(`\n\t\tINSERT INTO node_brain_config\n\t\t\t(node_id, provider, model, temperature, max_tokens, system_prompt, extra_config, updated_at)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, unixepoch('now'))\n\t\tON CONFLICT(node_id) DO UPDATE SET\n\t\t\tprovider=excluded.provider, model=excluded.model,\n\t\t\ttemperature=excluded.temperature, max_tokens=excluded.max_tokens,\n\t\t\tsystem_prompt=excluded.system_prompt, extra_config=excluded.extra_config,\n\t\t\tupdated_at=excluded.updated_at`,\n\t\tc.NodeID, c.Provider, c.Model, c.Temperature,\n\t\tc.MaxTokens, c.SystemPrompt, string(extraJSON))\n\treturn err\n}\n\n// GetBrainHistory returns recent brain execution log for a node.\nfunc (d *DB) GetBrainHistory(nodeID string, limit int) ([]*BrainExecutionEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT exec_id, node_id, provider, model, prompt_tokens, completion_tokens,\n\t\t latency_ms, success, error_text, executed_at, cycle\n\t\tFROM brain_execution_log WHERE node_id = ?\n\t\tORDER BY executed_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*BrainExecutionEntry\n\tfor rows.Next() {\n\t\te := &BrainExecutionEntry{}\n\t\tvar success int\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.Provider, &e.Model,\n\t\t\t&e.PromptTokens, &e.CompletionTokens, &e.LatencyMS,\n\t\t\t&success, &e.ErrorText, &e.ExecutedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/db/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/ go.mod go.sum\ngit commit -m \"feat(db): add Go SQLite read layer for Omega state + memory DBs\"\n```\n\n---\n\n## Task 4: Go Connect-RPC Handler\n\n**Files:**\n- Create: `internal/handler/orchestrator.go`\n\n- [ ] **Step 1: Write `internal/handler/orchestrator.go`**\n\n```go\n// Package handler implements the OrchestratorService Connect-RPC handler.\npackage handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n)\n\n// Ensure interface satisfaction at compile time.\n// Buf's connectrpc/go plugin generates the handler interface in the same package as the proto types.\nvar _ omegav1.OrchestratorServiceHandler = (*OrchestratorHandler)(nil)\n\n// OrchestratorHandler implements OrchestratorService.\ntype OrchestratorHandler struct {\n\tdb *db.DB\n\tmu sync.Mutex\n\torchCmd *exec.Cmd\n\torchRunning bool\n}\n\n// New creates an OrchestratorHandler backed by the given DB.\nfunc New(database *db.DB) *OrchestratorHandler {\n\treturn &OrchestratorHandler{db: database}\n}\n\n// tsFromUnix converts a float64 Unix timestamp to a protobuf Timestamp.\nfunc tsFromUnix(u float64) *timestamppb.Timestamp {\n\tif u == 0 {\n\t\treturn nil\n\t}\n\tsec := int64(u)\n\tnsec := int32((u - float64(sec)) * 1e9)\n\treturn timestamppb.New(time.Unix(sec, int64(nsec)))\n}\n\nfunc tsFromUnixPtr(u *float64) *timestamppb.Timestamp {\n\tif u == nil {\n\t\treturn nil\n\t}\n\treturn tsFromUnix(*u)\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetHealth(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetHealthRequest],\n) (*connect.Response[omegav1.GetHealthResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetHealthResponse{\n\t\tHealth: dbHealthToProto(health),\n\t}), nil\n}\n\nfunc dbHealthToProto(h *db.SystemHealth) *omegav1.SystemHealth {\n\treturn &omegav1.SystemHealth{\n\t\tStatus: h.Status,\n\t\tCompositeScore: h.CompositeScore,\n\t\tAvgNodeHealth: h.AvgNodeHealth,\n\t\tNodeCount: h.NodeCount,\n\t\tOpenIssues: h.OpenIssues,\n\t\tErrorIssues: h.ErrorIssues,\n\t\tUptimeSeconds: h.UptimeSeconds,\n\t\tTotalCycles: h.TotalCycles,\n\t}\n}\n\n// ── Nodes ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListNodes(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodesRequest],\n) (*connect.Response[omegav1.ListNodesResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoNodes []*omegav1.Node\n\tfor _, n := range nodes {\n\t\tprotoNodes = append(protoNodes, dbNodeToProto(n))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodesResponse{Nodes: protoNodes}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNode(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeRequest],\n) (*connect.Response[omegav1.GetNodeResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\texecs, _ := h.db.GetExecutions(req.Msg.NodeId, 10)\n\timps, _ := h.db.GetImprovements(req.Msg.NodeId, 20)\n\thistory, _ := h.db.LatencyHistory(req.Msg.NodeId, 100)\n\n\tresp := &omegav1.GetNodeResponse{\n\t\tNode: dbNodeToProto(n),\n\t\tImprovements: dbImpsToProto(imps),\n\t}\n\tfor _, e := range execs {\n\t\tresp.RecentExecutions = append(resp.RecentExecutions, dbExecToProto(e))\n\t}\n\tfor _, p := range history {\n\t\tresp.LatencyHistory = append(resp.LatencyHistory, &omegav1.LatencyPoint{\n\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\tDurationMs: p.DurationMS,\n\t\t\tSuccess: p.Success,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) ListNodeExecutions(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodeExecutionsRequest],\n) (*connect.Response[omegav1.ListNodeExecutionsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\texecs, err := h.db.GetExecutions(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoExecs []*omegav1.ExecutionRecord\n\tfor _, e := range execs {\n\t\tprotoExecs = append(protoExecs, dbExecToProto(e))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodeExecutionsResponse{Executions: protoExecs}), nil\n}\n\nfunc dbNodeToProto(n *db.Node) *omegav1.Node {\n\tnode := &omegav1.Node{\n\t\tNodeId: n.NodeID,\n\t\tName: n.Name,\n\t\tVersion: n.Version,\n\t\tCapabilities: n.Capabilities,\n\t\tHealth: n.Health,\n\t\tStatus: n.Status,\n\t\tRegisteredAt: tsFromUnix(n.RegisteredAt),\n\t\tLastUpdated: tsFromUnix(n.LastUpdated),\n\t\tExecutionsTotal: n.ExecutionsTotal,\n\t\tErrorRate: n.ErrorRate,\n\t\tAvgLatencyMs: n.AvgLatencyMS,\n\t\tP95LatencyMs: n.P95LatencyMS,\n\t\tImprovementCount: n.ImprovementCount,\n\t}\n\tif n.LastExecution != nil {\n\t\tnode.LastExecution = dbExecToProto(n.LastExecution)\n\t}\n\treturn node\n}\n\nfunc dbExecToProto(e *db.Execution) *omegav1.ExecutionRecord {\n\trec := &omegav1.ExecutionRecord{\n\t\tExecId: e.ExecID,\n\t\tNodeId: e.NodeID,\n\t\tNodeName: e.NodeName,\n\t\tTraceId: e.TraceID,\n\t\tSpanId: e.SpanID,\n\t\tAction: e.Action,\n\t\tStartedAt: tsFromUnix(e.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(e.EndedAt),\n\t\tSuccess: e.Success,\n\t\tErrorText: e.ErrorText,\n\t\tMetrics: e.Metrics,\n\t\tCycle: e.Cycle,\n\t}\n\tif e.DurationMS != nil {\n\t\trec.DurationMs = *e.DurationMS\n\t}\n\treturn rec\n}\n\n// ── Traces ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListTraces(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListTracesRequest],\n) (*connect.Response[omegav1.ListTracesResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 20\n\t}\n\ttraces, err := h.db.RecentTraces(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoTraces []*omegav1.TraceSummary\n\tfor _, t := range traces {\n\t\tprotoTraces = append(protoTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(&omegav1.ListTracesResponse{Traces: protoTraces}), nil\n}\n\nfunc (h *OrchestratorHandler) GetTrace(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetTraceRequest],\n) (*connect.Response[omegav1.GetTraceResponse], error) {\n\tspans, err := h.db.GetTraceSpans(req.Msg.TraceId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tif len(spans) == 0 {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"trace not found: %s\", req.Msg.TraceId))\n\t}\n\tvar protoSpans []*omegav1.Span\n\tfor _, s := range spans {\n\t\tprotoSpans = append(protoSpans, dbSpanToProto(s))\n\t}\n\treturn connect.NewResponse(&omegav1.GetTraceResponse{Spans: protoSpans}), nil\n}\n\nfunc dbTraceSummaryToProto(t *db.TraceSummary) *omegav1.TraceSummary {\n\treturn &omegav1.TraceSummary{\n\t\tTraceId: t.TraceID,\n\t\tTraceStarted: tsFromUnix(t.TraceStarted),\n\t\tTraceEnded: tsFromUnixPtr(t.TraceEnded),\n\t\tTotalDurationMs: t.TotalDurationMS,\n\t\tSpanCount: t.SpanCount,\n\t\tErrorSpans: t.ErrorSpans,\n\t\tCycle: t.Cycle,\n\t}\n}\n\nfunc dbSpanToProto(s *db.Span) *omegav1.Span {\n\tspan := &omegav1.Span{\n\t\tSpanId: s.SpanID,\n\t\tTraceId: s.TraceID,\n\t\tParentSpanId: s.ParentSpanID,\n\t\tNodeId: s.NodeID,\n\t\tNodeName: s.NodeName,\n\t\tOperation: s.Operation,\n\t\tStartedAt: tsFromUnix(s.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(s.EndedAt),\n\t\tStatus: s.Status,\n\t\tCycle: s.Cycle,\n\t}\n\tif s.DurationMS != nil {\n\t\tspan.DurationMs = *s.DurationMS\n\t}\n\treturn span\n}\n\n// ── Metrics ───────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetMetrics(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsRequest],\n) (*connect.Response[omegav1.GetMetricsResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tcosts, _ := h.db.GetCosts()\n\tissues, _ := h.db.GetIssues(\"open\")\n\ttraces, _ := h.db.RecentTraces(5)\n\n\tresp := &omegav1.GetMetricsResponse{System: dbHealthToProto(health)}\n\tfor _, n := range nodes {\n\t\tresp.Nodes = append(resp.Nodes, dbNodeToProto(n))\n\t}\n\tfor _, c := range costs {\n\t\tresp.Costs = append(resp.Costs, dbCostToProto(c))\n\t}\n\tfor _, i := range issues {\n\t\tresp.OpenIssues = append(resp.OpenIssues, dbIssueToProto(i))\n\t}\n\tfor _, t := range traces {\n\t\tresp.RecentTraces = append(resp.RecentTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) GetMetricsHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsHistoryRequest],\n) (*connect.Response[omegav1.GetMetricsHistoryResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar histories []*omegav1.NodeLatencyHistory\n\tfor _, n := range nodes {\n\t\tif req.Msg.NodeId != \"\" && n.NodeID != req.Msg.NodeId {\n\t\t\tcontinue\n\t\t}\n\t\thistory, _ := h.db.LatencyHistory(n.NodeID, 100)\n\t\tnh := &omegav1.NodeLatencyHistory{NodeId: n.NodeID, Name: n.Name}\n\t\tfor _, p := range history {\n\t\t\tnh.History = append(nh.History, &omegav1.LatencyPoint{\n\t\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\t\tDurationMs: p.DurationMS,\n\t\t\t\tSuccess: p.Success,\n\t\t\t})\n\t\t}\n\t\thistories = append(histories, nh)\n\t}\n\treturn connect.NewResponse(&omegav1.GetMetricsHistoryResponse{NodeHistories: histories}), nil\n}\n\n// ── Issues ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListIssues(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListIssuesRequest],\n) (*connect.Response[omegav1.ListIssuesResponse], error) {\n\tissues, err := h.db.GetIssues(req.Msg.StateFilter)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoIssues []*omegav1.Issue\n\tfor _, i := range issues {\n\t\tprotoIssues = append(protoIssues, dbIssueToProto(i))\n\t}\n\treturn connect.NewResponse(&omegav1.ListIssuesResponse{Issues: protoIssues}), nil\n}\n\nfunc dbIssueToProto(i *db.Issue) *omegav1.Issue {\n\tissue := &omegav1.Issue{\n\t\tIssueId: i.IssueID,\n\t\tDetector: i.Detector,\n\t\tSeverity: i.Severity,\n\t\tDescription: i.Description,\n\t\tState: i.State,\n\t\tOpenedAt: tsFromUnix(i.OpenedAt),\n\t\tResolvedAt: tsFromUnixPtr(i.ResolvedAt),\n\t\tCycleOpened: i.CycleOpened,\n\t}\n\tif i.CycleResolved != nil {\n\t\tissue.CycleResolved = *i.CycleResolved\n\t}\n\treturn issue\n}\n\n// ── Activity ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListActivity(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListActivityRequest],\n) (*connect.Response[omegav1.ListActivityResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.RecentActivity(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.ActivityEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.ActivityEntry{\n\t\t\tLogId: e.LogID,\n\t\t\tActionType: e.ActionType,\n\t\t\tEntityType: e.EntityType,\n\t\t\tEntityId: e.EntityID,\n\t\t\tRecordedAt: tsFromUnix(e.RecordedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.ListActivityResponse{Entries: protoEntries}), nil\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListImprovements(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListImprovementsRequest],\n) (*connect.Response[omegav1.ListImprovementsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\timps, err := h.db.GetImprovements(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.ListImprovementsResponse{\n\t\tImprovements: dbImpsToProto(imps),\n\t}), nil\n}\n\nfunc dbImpsToProto(imps []*db.Improvement) []*omegav1.ImprovementRecord {\n\tvar result []*omegav1.ImprovementRecord\n\tfor _, imp := range imps {\n\t\tresult = append(result, &omegav1.ImprovementRecord{\n\t\t\tImproveId: imp.ImproveID,\n\t\t\tNodeId: imp.NodeID,\n\t\t\tNodeName: imp.NodeName,\n\t\t\tFromVersion: imp.FromVersion,\n\t\t\tToVersion: imp.ToVersion,\n\t\t\tTriggeredBy: imp.TriggeredBy,\n\t\t\tRecordedAt: tsFromUnix(imp.RecordedAt),\n\t\t\tCycle: imp.Cycle,\n\t\t\tBeforeMetrics: imp.BeforeMetrics,\n\t\t\tAfterMetrics: imp.AfterMetrics,\n\t\t})\n\t}\n\treturn result\n}\n\n// ── Memory ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetNodeMemory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeMemoryRequest],\n) (*connect.Response[omegav1.GetNodeMemoryResponse], error) {\n\t// Resolve node name for namespace\n\tvar namespace string\n\tif req.Msg.NodeId != \"\" {\n\t\tn, err := h.db.GetNode(req.Msg.NodeId)\n\t\tif err != nil {\n\t\t\treturn nil, connect.NewError(connect.CodeNotFound, err)\n\t\t}\n\t\tnamespace = n.Name\n\t}\n\n\tepCount, semCount, concepts, episodes, err := h.db.GetMemoryStats(namespace)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\tresp := &omegav1.GetNodeMemoryResponse{\n\t\tNamespace: namespace,\n\t\tEpisodicCount: epCount,\n\t\tSemanticCount: semCount,\n\t}\n\tfor _, c := range concepts {\n\t\tresp.TopSemantic = append(resp.TopSemantic, &omegav1.SemanticConcept{\n\t\t\tConcept: c.Concept,\n\t\t\tContent: c.Content,\n\t\t\tConfidence: c.Confidence,\n\t\t\tEvidenceCount: c.EvidenceCount,\n\t\t\tTags: c.Tags,\n\t\t})\n\t}\n\tfor _, e := range episodes {\n\t\tresp.RecentEpisodes = append(resp.RecentEpisodes, &omegav1.EpisodeEntry{\n\t\t\tEpisodeId: e.EpisodeID,\n\t\t\tEventType: e.EventType,\n\t\t\tTimestamp: tsFromUnix(e.Timestamp),\n\t\t\tCycle: e.Cycle,\n\t\t\tImportance: e.Importance,\n\t\t\tTags: e.Tags,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetConvergence(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetConvergenceRequest],\n) (*connect.Response[omegav1.GetConvergenceResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 100\n\t}\n\tpoints, err := h.db.GetConvergence(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoPoints []*omegav1.ConvergencePoint\n\tfor _, p := range points {\n\t\tprotoPoints = append(protoPoints, &omegav1.ConvergencePoint{\n\t\t\tCycle: p.Cycle,\n\t\t\tTimestamp: tsFromUnix(p.Timestamp),\n\t\t\tScore: p.Score,\n\t\t\tPipelineMs: p.PipelineMS,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetConvergenceResponse{Points: protoPoints}), nil\n}\n\n// ── Costs ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetCosts(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetCostsRequest],\n) (*connect.Response[omegav1.GetCostsResponse], error) {\n\tcosts, err := h.db.GetCosts()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoCosts []*omegav1.CostEntry\n\tfor _, c := range costs {\n\t\tprotoCosts = append(protoCosts, dbCostToProto(c))\n\t}\n\treturn connect.NewResponse(&omegav1.GetCostsResponse{Costs: protoCosts}), nil\n}\n\nfunc dbCostToProto(c *db.CostEntry) *omegav1.CostEntry {\n\treturn &omegav1.CostEntry{\n\t\tProvider: c.Provider,\n\t\tNodeId: c.NodeID,\n\t\tCalls: c.Calls,\n\t\tTotalMs: c.TotalMS,\n\t\tTotalCostUsd: c.TotalCostUSD,\n\t}\n}\n\n// ── Feedback ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) SubmitFeedback(\n\tctx context.Context,\n\treq *connect.Request[omegav1.SubmitFeedbackRequest],\n) (*connect.Response[omegav1.SubmitFeedbackResponse], error) {\n\t// Shell out to Python to store feedback in MemoryKernel\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-c\", fmt.Sprintf(`\nfrom omega.core.memory import MemoryKernel\nm = MemoryKernel(db_path='/tmp/omega_victoria_memory.db')\nm.store_episode('human_feedback', {'text': %q, 'target_node': %q}, tags=['feedback','human'], importance=0.9)\nprint('ok')\n`, req.Msg.Text, req.Msg.TargetNode))\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"feedback storage failed: %w\", err))\n\t}\n\treturn connect.NewResponse(&omegav1.SubmitFeedbackResponse{Ok: true}), nil\n}\n\n// ── Orchestrator control ──────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StartOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StartOrchestratorRequest],\n) (*connect.Response[omegav1.StartOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif h.orchRunning {\n\t\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\t\tStarted: false, Message: \"already running\",\n\t\t}), nil\n\t}\n\theartbeat := req.Msg.HeartbeatSecs\n\tif heartbeat <= 0 {\n\t\theartbeat = 120\n\t}\n\tcmd := exec.Command(\"python3\", \"-m\", \"omega.examples.victoria_main\",\n\t\t\"--heartbeat\", fmt.Sprintf(\"%d\", heartbeat))\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"start orchestrator: %w\", err))\n\t}\n\th.orchCmd = cmd\n\th.orchRunning = true\n\t// Watch for exit\n\tgo func() {\n\t\tcmd.Wait() //nolint:errcheck\n\t\th.mu.Lock()\n\t\th.orchRunning = false\n\t\th.orchCmd = nil\n\t\th.mu.Unlock()\n\t}()\n\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\tStarted: true, Message: \"started\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) StopOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StopOrchestratorRequest],\n) (*connect.Response[omegav1.StopOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif !h.orchRunning || h.orchCmd == nil {\n\t\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\t\tStopped: false, Message: \"not running\",\n\t\t}), nil\n\t}\n\tif err := h.orchCmd.Process.Kill(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\th.orchRunning = false\n\th.orchCmd = nil\n\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\tStopped: true, Message: \"stopped\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) TriggerHeartbeat(\n\tctx context.Context,\n\treq *connect.Request[omegav1.TriggerHeartbeatRequest],\n) (*connect.Response[omegav1.TriggerHeartbeatResponse], error) {\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-m\", \"omega.examples.victoria_main\", \"--once\")\n\tgo cmd.Run() //nolint:errcheck\n\treturn connect.NewResponse(&omegav1.TriggerHeartbeatResponse{\n\t\tTriggered: true, Message: \"triggered single heartbeat\",\n\t}), nil\n}\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StreamEvents(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StreamEventsRequest],\n\tstream *connect.ServerStream[omegav1.EventResponse],\n) error {\n\tinterval := time.Duration(req.Msg.PollIntervalMs) * time.Millisecond\n\tif interval < 500*time.Millisecond {\n\t\tinterval = 2 * time.Second\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase <-ticker.C:\n\t\t\thealth, err := h.db.SystemHealth()\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := stream.Send(&omegav1.EventResponse{\n\t\t\t\tType: \"health_update\",\n\t\t\t\tPayload: &omegav1.EventResponse_HealthUpdate{\n\t\t\t\t\tHealthUpdate: dbHealthToProto(health),\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ── Brain config RPCs ─────────────────────────────────────────────────────────\n\n// knownProviders lists all supported LLM providers and their available models.\nvar knownProviders = []*omegav1.ProviderInfo{\n\t{Id: \"anthropic\", DisplayName: \"Anthropic\", Models: []string{\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"openai\", DisplayName: \"OpenAI\", Models: []string{\"gpt-4o\", \"gpt-4o-mini\", \"o1\", \"o3-mini\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"google\", DisplayName: \"Google\", Models: []string{\"gemini-2.0-flash\", \"gemini-2.0-pro\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"deepseek\", DisplayName: \"DeepSeek\", Models: []string{\"deepseek-chat\", \"deepseek-reasoner\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"ollama\", DisplayName: \"Ollama (local)\", Models: []string{\"llama3.3\", \"qwen2.5-coder\", \"mistral\"}, RequiresApiKey: false, Available: true},\n}\n\nfunc (h *OrchestratorHandler) ListAvailableProviders(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListAvailableProvidersRequest],\n) (*connect.Response[omegav1.ListAvailableProvidersResponse], error) {\n\treturn connect.NewResponse(&omegav1.ListAvailableProvidersResponse{\n\t\tProviders: knownProviders,\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNodeConfig(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeConfigRequest],\n) (*connect.Response[omegav1.GetNodeConfigResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\tbrain, err := h.db.GetBrainConfig(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetNodeConfigResponse{\n\t\tConfig: dbNodeConfigToProto(n, brain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) UpdateNodeBrain(\n\tctx context.Context,\n\treq *connect.Request[omegav1.UpdateNodeBrainRequest],\n) (*connect.Response[omegav1.UpdateNodeBrainResponse], error) {\n\tbrain := &db.BrainConfig{\n\t\tNodeID: req.Msg.NodeId,\n\t\tProvider: req.Msg.Brain.Provider,\n\t\tModel: req.Msg.Brain.Model,\n\t\tTemperature: req.Msg.Brain.Temperature,\n\t\tMaxTokens: int64(req.Msg.Brain.MaxTokens),\n\t\tSystemPrompt: req.Msg.Brain.SystemPrompt,\n\t\tExtraConfig: req.Msg.Brain.ExtraConfig,\n\t}\n\tif err := h.db.SetBrainConfig(brain); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tn, _ := h.db.GetNode(req.Msg.NodeId)\n\tupdatedBrain, _ := h.db.GetBrainConfig(req.Msg.NodeId)\n\treturn connect.NewResponse(&omegav1.UpdateNodeBrainResponse{\n\t\tConfig: dbNodeConfigToProto(n, updatedBrain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetBrainExecutionHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetBrainExecutionHistoryRequest],\n) (*connect.Response[omegav1.GetBrainExecutionHistoryResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.GetBrainHistory(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.BrainExecutionEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.BrainExecutionEntry{\n\t\t\tExecId: e.ExecID,\n\t\t\tNodeId: e.NodeID,\n\t\t\tProvider: e.Provider,\n\t\t\tModel: e.Model,\n\t\t\tPromptTokens: e.PromptTokens,\n\t\t\tCompletionTokens: e.CompletionTokens,\n\t\t\tLatencyMs: e.LatencyMS,\n\t\t\tSuccess: e.Success,\n\t\t\tErrorText: e.ErrorText,\n\t\t\tExecutedAt: tsFromUnix(e.ExecutedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetBrainExecutionHistoryResponse{Entries: protoEntries}), nil\n}\n\nfunc dbNodeConfigToProto(n *db.Node, brain *db.BrainConfig) *omegav1.NodeConfig {\n\tconfig := &omegav1.NodeConfig{\n\t\tNodeId: n.NodeID,\n\t\tNodeType: n.Name,\n\t\tVersion: n.Version,\n\t\tBrain: &omegav1.BrainConfig{\n\t\t\tProvider: brain.Provider,\n\t\t\tModel: brain.Model,\n\t\t\tTemperature: brain.Temperature,\n\t\t\tMaxTokens: int32(brain.MaxTokens),\n\t\t\tSystemPrompt: brain.SystemPrompt,\n\t\t\tExtraConfig: brain.ExtraConfig,\n\t\t},\n\t\tUpdatedAt: tsFromUnix(brain.UpdatedAt),\n\t}\n\treturn config\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/handler/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/handler/\ngit commit -m \"feat(handler): implement OrchestratorService + brain config Connect-RPC handlers\"\n```\n\n---\n\n## Task 5: Go API Server Entry Point\n\n**Files:**\n- Create: `cmd/omega-api/main.go`\n\n- [ ] **Step 1: Write `cmd/omega-api/main.go`**\n\n```go\n// Command omega-api serves the Omega dashboard Connect-RPC API.\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"connectrpc.com/connect\"\n\tconnectcors \"connectrpc.com/connect/cors\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n\t\"github.com/benebsworth/omega/internal/handler\"\n)\n\nfunc main() {\n\tdatabase, err := db.New(db.StateDBPath, db.MemoryDBPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"open DB: %v\", err)\n\t}\n\tdefer database.Close()\n\n\th := handler.New(database)\n\tmux := http.NewServeMux()\n\n\t// Buf's connectrpc/go plugin generates NewOrchestratorServiceHandler in the same package.\n\tpath, svcHandler := omegav1.NewOrchestratorServiceHandler(h,\n\t\tconnect.WithCompressMinBytes(1024),\n\t)\n\tmux.Handle(path, svcHandler)\n\n\t// CORS — use connectrpc.com/connect/cors helpers to include streaming headers.\n\tcorsHandler := withCORS(mux)\n\n\taddr := \":8080\"\n\tlog.Printf(\"Omega API listening on %s (Connect-RPC)\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, h2c.NewHandler(corsHandler, &http2.Server{})))\n}\n\nfunc withCORS(h http.Handler) http.Handler {\n\tallowedHeaders := append(connectcors.AllowedHeaders(), \"X-User-Agent\")\n\texposedHeaders := connectcors.ExposedHeaders()\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", strings.Join(allowedHeaders, \", \"))\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", strings.Join(exposedHeaders, \", \"))\n\t\tif r.Method == http.MethodOptions {\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\treturn\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t})\n}\n```\n\n- [ ] **Step 2: Build and verify**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api/...\n```\n\nExpected: produces `./omega-api` (or similar) without errors.\n\n- [ ] **Step 3: Smoke test — start server and verify it responds**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\ncurl -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" \\\n -d '{}' | python3 -m json.tool\nkill %1\n```\n\nExpected: JSON response with health data (or empty `{}` if DB doesn't exist yet).\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add cmd/\ngit commit -m \"feat(cmd): add omega-api Connect-RPC server entry point\"\n```\n\n---\n\n## Task 6: Frontend Scaffold + Connect-ES Setup\n\n**Files:**\n- Create: `dashboard/` — Vite React TS project\n\n- [ ] **Step 1: Scaffold Vite project at repo root**\n\n```bash\ncd /Users/benebsworth/projects/omega\nnpm create vite@latest dashboard -- --template react-ts\n```\n\n- [ ] **Step 2: Install dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm install\nnpm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf\nnpm install react-router-dom recharts lucide-react\nnpm install -D tailwindcss postcss autoprefixer @types/node\nnpx tailwindcss init -p\n```\n\n- [ ] **Step 3: Configure Tailwind — `tailwind.config.js`**\n\n```js\n/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n theme: {\n extend: {\n colors: {\n surface: {\n 900: \"#0f1117\",\n 800: \"#161b27\",\n 700: \"#1e2533\",\n 600: \"#252d3d\",\n },\n },\n },\n },\n plugins: [],\n}\n```\n\n- [ ] **Step 4: Replace `src/index.css`**\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n- [ ] **Step 5: Configure Vite proxy — `vite.config.ts`**\n\n```ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n port: 5173,\n proxy: {\n // Proxy all Connect-RPC calls to the Go server\n \"/omega.v1\": {\n target: \"http://localhost:8080\",\n changeOrigin: true,\n },\n },\n },\n});\n```\n\n- [ ] **Step 6: Write `src/client.ts` — Connect transport + service client**\n\n```typescript\nimport { createConnectTransport } from \"@connectrpc/connect-web\";\nimport { createClient } from \"@connectrpc/connect\";\nimport { OrchestratorService } from \"./gen/omega/v1/omega_service_pb\";\n\n// In dev, Vite proxies /omega.v1/* → http://localhost:8080\n// In production, VITE_API_URL should point to the Go server\nconst baseUrl = import.meta.env.VITE_API_URL ?? \"\";\n\nexport const transport = createConnectTransport({\n baseUrl,\n // Use the Connect protocol (works over HTTP/1.1 without gRPC-web)\n});\n\nexport const client = createClient(OrchestratorService, transport);\n```\n\n- [ ] **Step 7: Commit scaffold**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/\ngit commit -m \"feat(dashboard): scaffold Vite React TS with Connect-ES + Tailwind\"\n```\n\n---\n\n## Task 7: Layout Components\n\n**Files:**\n- Create: `dashboard/src/components/layout/Sidebar.tsx`\n- Create: `dashboard/src/components/layout/Header.tsx`\n- Create: `dashboard/src/App.tsx`\n\n- [ ] **Step 1: Write `Sidebar.tsx`**\n\n```tsx\nimport { NavLink } from \"react-router-dom\";\nimport {\n LayoutDashboard, Server, GitBranch, BarChart2,\n AlertTriangle, Brain, TrendingUp,\n} from \"lucide-react\";\n\nconst NAV = [\n { to: \"/\", icon: LayoutDashboard, label: \"Dashboard\" },\n { to: \"/nodes\", icon: Server, label: \"Nodes\" },\n { to: \"/traces\", icon: GitBranch, label: \"Traces\" },\n { to: \"/metrics\", icon: BarChart2, label: \"Metrics\" },\n { to: \"/issues\", icon: AlertTriangle, label: \"Issues\" },\n { to: \"/memory\", icon: Brain, label: \"Memory\" },\n { to: \"/convergence\", icon: TrendingUp, label: \"Convergence\" },\n];\n\nexport default function Sidebar() {\n return (\n \n );\n}\n```\n\n- [ ] **Step 2: Write `Header.tsx`**\n\n```tsx\nimport { useState } from \"react\";\nimport { client } from \"../../client\";\nimport { Play, Square, Zap } from \"lucide-react\";\n\ninterface HeaderProps {\n systemStatus?: string;\n connected?: boolean;\n}\n\nexport default function Header({ systemStatus, connected }: HeaderProps) {\n const [loading, setLoading] = useState(null);\n\n const statusColor = {\n healthy: \"bg-green-500\",\n degraded: \"bg-yellow-500\",\n critical: \"bg-red-500\",\n no_nodes: \"bg-gray-500\",\n }[systemStatus ?? \"no_nodes\"] ?? \"bg-gray-500\";\n\n async function action(name: \"start\" | \"stop\" | \"trigger\") {\n setLoading(name);\n try {\n if (name === \"start\") await client.startOrchestrator({ heartbeatSecs: 120 });\n else if (name === \"stop\") await client.stopOrchestrator({});\n else await client.triggerHeartbeat({});\n } finally {\n setLoading(null);\n }\n }\n\n return (\n
\n
\n \n \n {systemStatus ?? \"unknown\"}\n \n {connected !== undefined && (\n \n {connected ? \"streaming\" : \"offline\"}\n \n )}\n
\n
\n {([\"start\", \"trigger\", \"stop\"] as const).map((name) => (\n action(name)}\n disabled={!!loading}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors disabled:opacity-50 ${\n name === \"start\" ? \"bg-green-700 hover:bg-green-600\" :\n name === \"trigger\" ? \"bg-indigo-700 hover:bg-indigo-600\" :\n \"bg-red-800 hover:bg-red-700\"\n }`}\n >\n {name === \"start\" ? : name === \"stop\" ? : }\n {name.charAt(0).toUpperCase() + name.slice(1)}\n \n ))}\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Write `App.tsx`**\n\n```tsx\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Sidebar from \"./components/layout/Sidebar\";\nimport Header from \"./components/layout/Header\";\nimport { client } from \"./client\";\nimport type { SystemHealth } from \"./gen/omega/v1/types_pb\";\nimport Dashboard from \"./pages/Dashboard\";\nimport Nodes from \"./pages/Nodes\";\nimport Traces from \"./pages/Traces\";\nimport Metrics from \"./pages/Metrics\";\nimport Issues from \"./pages/Issues\";\nimport Memory from \"./pages/Memory\";\nimport Convergence from \"./pages/Convergence\";\n\nexport default function App() {\n const [health, setHealth] = useState(null);\n const [streamConnected, setStreamConnected] = useState(false);\n const abortRef = useRef(null);\n\n useEffect(() => {\n startStream();\n return () => abortRef.current?.abort();\n }, []);\n\n async function startStream() {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n setStreamConnected(true);\n for await (const event of client.streamEvents(\n { pollIntervalMs: 2000 },\n { signal: controller.signal }\n )) {\n if (event.payload.case === \"healthUpdate\") {\n setHealth(event.payload.value);\n }\n }\n } catch {\n setStreamConnected(false);\n // Reconnect after 3s\n setTimeout(startStream, 3000);\n }\n }\n\n return (\n \n
\n \n
\n \n
\n \n } />\n } />\n } />\n } />\n } />\n } />\n } />\n } />\n \n
\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 4: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/src/\ngit commit -m \"feat(dashboard): add layout + App router with server-streaming health\"\n```\n\n---\n\n## Task 8: Dashboard Page\n\n**Files:**\n- Create: `dashboard/src/pages/Dashboard.tsx`\n- Create: `dashboard/src/components/NodeCard.tsx`\n\n- [ ] **Step 1: Write `NodeCard.tsx`**\n\n```tsx\nimport { LineChart, Line, ResponsiveContainer } from \"recharts\";\nimport type { Node } from \"../gen/omega/v1/types_pb\";\n\nfunction healthColor(h: number) {\n if (h >= 0.8) return \"border-green-600 bg-green-900/20\";\n if (h >= 0.6) return \"border-yellow-600 bg-yellow-900/20\";\n return \"border-red-600 bg-red-900/20\";\n}\n\nfunction healthText(h: number) {\n if (h >= 0.8) return \"text-green-400\";\n if (h >= 0.6) return \"text-yellow-400\";\n return \"text-red-400\";\n}\n\ninterface NodeCardProps { node: Node }\n\nexport default function NodeCard({ node }: NodeCardProps) {\n const sparkData = node.latencyHistory.slice(-20).map((p) => ({ v: p.durationMs }));\n const lastExecAgo = node.lastExecution?.startedAt\n ? Math.round((Date.now() / 1000 - Number(node.lastExecution.startedAt.seconds)) / 60)\n : null;\n\n return (\n
\n
\n
\n

{node.name}

\n

v{node.version}

\n
\n \n {(node.health * 100).toFixed(0)}%\n \n
\n {sparkData.length > 1 && (\n
\n \n \n = 0.8 ? \"#4ade80\" : node.health >= 0.6 ? \"#facc15\" : \"#f87171\"}\n dot={false} strokeWidth={1.5} />\n \n \n
\n )}\n
\n err: {(node.errorRate * 100).toFixed(1)}%\n {lastExecAgo !== null ? `${lastExecAgo}m ago` : \"never\"}\n
\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Dashboard.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport NodeCard from \"../components/NodeCard\";\nimport type { SystemHealth, Node, ActivityEntry } from \"../gen/omega/v1/types_pb\";\n\ninterface DashboardProps { health: SystemHealth | null }\n\nexport default function Dashboard({ health }: DashboardProps) {\n const [nodes, setNodes] = useState([]);\n const [activity, setActivity] = useState([]);\n\n useEffect(() => {\n client.listNodes({}).then((r) => setNodes(r.nodes)).catch(console.error);\n client.listActivity({ limit: 20 }).then((r) => setActivity(r.entries)).catch(console.error);\n }, []);\n\n const statusColor: Record = {\n healthy: \"text-green-400\",\n degraded: \"text-yellow-400\",\n critical: \"text-red-400\",\n };\n\n return (\n
\n {/* System stats */}\n
\n {[\n { label: \"Status\", value: health?.status ?? \"—\", cls: statusColor[health?.status ?? \"\"] ?? \"text-white\" },\n { label: \"Health Score\", value: health ? `${(health.compositeScore * 100).toFixed(1)}%` : \"—\", cls: \"text-white\" },\n { label: \"Total Cycles\", value: String(health?.totalCycles ?? \"—\"), cls: \"text-white\" },\n { label: \"Open Issues\", value: String(health?.openIssues ?? \"—\"), cls: (health?.openIssues ?? 0) > 0 ? \"text-red-400\" : \"text-white\" },\n ].map(({ label, value, cls }) => (\n
\n

{label}

\n

{value}

\n
\n ))}\n
\n\n {/* Node grid */}\n
\n

\n Nodes ({nodes.length})\n

\n
\n {nodes.map((n) => )}\n
\n {nodes.length === 0 && (\n

\n No nodes registered — run the orchestrator first.\n

\n )}\n
\n\n {/* Activity feed */}\n
\n

\n Recent Activity\n

\n
\n {activity.map((evt) => (\n
\n \n {evt.recordedAt\n ? new Date(Number(evt.recordedAt.seconds) * 1000).toLocaleTimeString()\n : \"—\"}\n \n {evt.actionType}\n {evt.entityId}\n
\n ))}\n {activity.length === 0 && (\n

No activity yet.

\n )}\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/Dashboard.tsx dashboard/src/components/NodeCard.tsx\ngit commit -m \"feat(dashboard): add Dashboard page using Connect-ES typed client\"\n```\n\n---\n\n## Task 9: Traces Page with Waterfall\n\n**Files:**\n- Create: `dashboard/src/components/TraceWaterfall.tsx`\n- Create: `dashboard/src/pages/Traces.tsx`\n\n- [ ] **Step 1: Write `TraceWaterfall.tsx`**\n\n```tsx\nimport type { Span } from \"../gen/omega/v1/types_pb\";\n\ninterface TraceWaterfallProps { spans: Span[] }\n\nexport default function TraceWaterfall({ spans }: TraceWaterfallProps) {\n if (!spans.length) return

No spans.

;\n\n const sorted = [...spans].sort(\n (a, b) => Number(a.startedAt?.seconds ?? 0) - Number(b.startedAt?.seconds ?? 0)\n );\n const minTs = Number(sorted[0]?.startedAt?.seconds ?? 0);\n const maxTs = sorted.reduce(\n (m, s) => Math.max(m, Number(s.endedAt?.seconds ?? s.startedAt?.seconds ?? 0)), 0\n );\n const totalMs = Math.max(1, (maxTs - minTs) * 1000);\n\n const depthMap = new Map();\n for (const span of sorted) {\n depthMap.set(span.spanId, span.parentSpanId ? (depthMap.get(span.parentSpanId) ?? 0) + 1 : 0);\n }\n\n return (\n
\n {sorted.map((span) => {\n const depth = depthMap.get(span.spanId) ?? 0;\n const offsetMs = (Number(span.startedAt?.seconds ?? 0) - minTs) * 1000;\n const durMs = span.durationMs;\n const leftPct = (offsetMs / totalMs) * 100;\n const widthPct = Math.max(0.5, (durMs / totalMs) * 100);\n const isErr = span.status === \"error\";\n\n return (\n
\n
\n {isErr ? \"✗\" : \"✓\"}{\" \"}\n {span.nodeName || span.operation}\n
\n
\n \n
\n
{durMs.toFixed(0)}ms
\n
\n );\n })}\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Traces.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport TraceWaterfall from \"../components/TraceWaterfall\";\nimport type { TraceSummary, Span } from \"../gen/omega/v1/types_pb\";\n\nexport default function Traces() {\n const [traces, setTraces] = useState([]);\n const [selected, setSelected] = useState(null);\n const [spans, setSpans] = useState([]);\n\n useEffect(() => {\n client.listTraces({ limit: 30 }).then((r) => setTraces(r.traces)).catch(console.error);\n }, []);\n\n function selectTrace(id: string) {\n setSelected(id);\n client.getTrace({ traceId: id }).then((r) => setSpans(r.spans)).catch(console.error);\n }\n\n return (\n
\n
\n
\n Recent Traces\n
\n {traces.map((t) => (\n \n ))}\n {traces.length === 0 &&

No traces yet

}\n
\n
\n {selected\n ? <>

trace: {selected}

\n :

Select a trace to view the waterfall

\n }\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/components/TraceWaterfall.tsx dashboard/src/pages/Traces.tsx\ngit commit -m \"feat(dashboard): add trace waterfall viewer with typed Connect-ES\"\n```\n\n---\n\n## Task 10: Remaining Pages + Brain Config UI\n\n**Files:**\n- Create: `dashboard/src/pages/Nodes.tsx`\n- Create: `dashboard/src/components/BrainConfigPanel.tsx`\n- Create: `dashboard/src/pages/Metrics.tsx`\n- Create: `dashboard/src/pages/Issues.tsx`\n- Create: `dashboard/src/pages/Memory.tsx`\n- Create: `dashboard/src/pages/Convergence.tsx`\n\n- [ ] **Step 1: Write `BrainConfigPanel.tsx` — model switcher component**\n\n```tsx\nimport { useState, useEffect } from \"react\";\nimport { client } from \"../client\";\nimport type { NodeConfig, ProviderInfo } from \"../gen/omega/v1/types_pb\";\n\ninterface Props { nodeId: string }\n\nexport default function BrainConfigPanel({ nodeId }: Props) {\n const [config, setConfig] = useState(null);\n const [providers, setProviders] = useState([]);\n const [saving, setSaving] = useState(false);\n const [draft, setDraft] = useState({ provider: \"\", model: \"\", temperature: 0.7, maxTokens: 4096 });\n\n useEffect(() => {\n if (!nodeId) return;\n client.getNodeConfig({ nodeId }).then((r) => {\n setConfig(r.config ?? null);\n if (r.config?.brain) {\n const b = r.config.brain;\n setDraft({ provider: b.provider, model: b.model, temperature: b.temperature, maxTokens: b.maxTokens });\n }\n });\n client.listAvailableProviders({}).then((r) => setProviders(r.providers));\n }, [nodeId]);\n\n const currentProvider = providers.find((p) => p.id === draft.provider);\n\n async function save() {\n setSaving(true);\n try {\n const r = await client.updateNodeBrain({\n nodeId,\n brain: { provider: draft.provider, model: draft.model, temperature: draft.temperature, maxTokens: draft.maxTokens, systemPrompt: config?.brain?.systemPrompt ?? \"\", extraConfig: {} },\n });\n setConfig(r.config ?? null);\n } finally {\n setSaving(false);\n }\n }\n\n if (!config) return
Loading brain config…
;\n\n return (\n
\n

\n 🧠 LLM Brain Config\n

\n
\n
\n \n setDraft({ ...draft, provider: e.target.value, model: \"\" })}\n >\n {providers.map((p) => )}\n \n
\n
\n \n setDraft({ ...draft, model: e.target.value })}\n >\n {(currentProvider?.models ?? []).map((m) => )}\n \n
\n
\n \n setDraft({ ...draft, temperature: parseFloat(e.target.value) })}\n />\n
\n
\n \n setDraft({ ...draft, maxTokens: parseInt(e.target.value, 10) })}\n />\n
\n
\n \n {saving ? \"Saving…\" : \"Apply Brain Config\"}\n \n
\n );\n}\n```\n\n- [ ] **Step 2: Write stubs for remaining pages (iterate to full implementations)**\n\nEach page follows the same pattern: call `client.methodName({})`, display results. Write full implementations using the generated types.\n\n`Nodes.tsx` — calls `client.listNodes`, `client.getNode`, `client.getMetricsHistory`; includes `` in the node detail panel\n`Metrics.tsx` — calls `client.getMetricsHistory`, renders per-node `LineChart` with recharts\n`Issues.tsx` — calls `client.listIssues({ stateFilter: \"open\" })`, renders sortable table\n`Memory.tsx` — calls `client.getNodeMemory({ nodeId: \"\" })` for global view, semantic concept list\n`Convergence.tsx` — calls `client.getConvergence`, `client.listImprovements`, renders score chart\n\nThese pages follow the same structure as shown in Tasks 8 and 9. Write them using the generated proto types from `./gen/omega/v1/types_pb`.\n\n**`Nodes.tsx` detail panel pattern:**\n```tsx\n// When a node is selected, show node stats + brain config side by side\n{selectedNode && (\n
\n
\n {/* node stats, latency chart, execution history */}\n
\n \n
\n)}\n```\n\n- [ ] **Step 2: Verify build**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run build 2>&1 | tail -10\n```\n\nExpected: `built in Xs` with no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/\ngit commit -m \"feat(dashboard): complete all 7 pages\"\n```\n\n---\n\n## Task 11: End-to-End Verification\n\n- [ ] **Step 1: Run one Python heartbeat to populate SQLite DBs**\n\n```bash\ncd /Users/benebsworth/projects/omega\npython -m omega.examples.victoria_main --once\n```\n\nExpected: heartbeat runs, data written to `/tmp/omega_victoria_state.db`.\n\n- [ ] **Step 2: Start Go API server**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\n```\n\n- [ ] **Step 3: Verify key endpoints**\n\n```bash\n# Health\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Nodes\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool | head -30\n\n# Traces\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListTraces \\\n -H \"Content-Type: application/json\" -d '{\"limit\": 5}' | python3 -m json.tool\n\n# Brain config — list providers\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListAvailableProviders \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Brain config — get node config (replace NODE_ID with a real node_id from ListNodes)\nNODE_ID=$(curl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['nodes'][0]['nodeId'])\" 2>/dev/null)\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetNodeConfig \\\n -H \"Content-Type: application/json\" -d \"{\\\"nodeId\\\": \\\"$NODE_ID\\\"}\" | python3 -m json.tool\n```\n\nExpected: nodes with health scores, at least one trace, providers list with 5 entries, node config defaulting to `claude-sonnet-4-6`.\n\n- [ ] **Step 4: Start frontend**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev &\nsleep 2\necho \"Dashboard at http://localhost:5173\"\n```\n\n- [ ] **Step 5: Verify via Playwright MCP**\n\nUse `mcp__plugin_playwright_playwright__browser_navigate` to visit `http://localhost:5173` and take a screenshot to confirm the dashboard loads.\n\n- [ ] **Step 6: Final commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add -A\ngit commit -m \"feat: complete Omega Go+Connect-RPC API and React dashboard\"\n```\n\n---\n\n## Quick-start (reference)\n\n```bash\n# Terminal 1: Go API\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api && ./omega-api\n\n# Terminal 2: React dashboard\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev\n\n# Terminal 3: Python orchestrator\npython -m omega.examples.victoria_main --heartbeat 60\n\n# Open dashboard\nopen http://localhost:5173\n```\n"}, "32efafaeb86e07db81ffa83405a6a051becac752": {"t": 1785764524.388475, "n": 99994, "h": "ff9f8018ae45c5d4", "c": "# Omega Web Dashboard — Go + Connect-RPC + Protobuf Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Build a Go Connect-RPC API server + React/TypeScript dashboard with Connect-ES that reads from Omega's SQLite state store and renders live visibility into the self-improving agent system.\n\n**Architecture:** Proto definitions are the contract. Go server (`cmd/omega-api`) reads from the shared SQLite DBs that the Python node layer writes to. React dashboard uses `@connectrpc/connect-web` to call the Go server directly — no REST, no WebSockets. Real-time updates use server-streaming RPC (`StreamEvents`).\n\n**Tech Stack:** Go 1.24+, `connectrpc.com/connect`, `modernc.org/sqlite`, `google.golang.org/protobuf`, Buf CLI; Vite 5, React 18, TypeScript, `@connectrpc/connect-web` v2, `@bufbuild/protobuf`, Tailwind CSS, shadcn/ui, recharts\n\n---\n\n## Key Data Facts (from existing Python codebase)\n\n- **State store DB**: `/tmp/omega_vectora_state.db`\n - Tables: `nodes`, `node_executions`, `traces`, `cost_events`, `issues`, `activity_log`, `improvement_log`, `config_revisions`\n- **Memory DB**: `/tmp/omega_vectora_memory.db`\n - Tables: `episodes`, `semantic_memories`, `memory_ratings`\n- **Convergence data**: `episodes WHERE event_type='cycle_summary'` contains `score`, `pipeline_ms` per cycle\n- **Node namespacing**: memory is namespaced by node name (e.g., `\"DataIngestionNode\"`)\n- **Toolchain available**: Go 1.26, Buf 1.47, Node 22, npm 10\n\n---\n\n## File Map\n\n### Proto definitions\n| File | Responsibility |\n|------|---------------|\n| `buf.yaml` | Buf workspace config |\n| `buf.gen.yaml` | Code generation config (Go + TypeScript) |\n| `proto/omega/v1/omega_service.proto` | OrchestratorService — all dashboard RPCs |\n| `proto/omega/v1/node_service.proto` | NodeService — inter-node communication contract |\n| `proto/omega/v1/types.proto` | Shared message types (Node, Span, Issue, etc.) |\n\n### Go backend\n| File | Responsibility |\n|------|---------------|\n| `go.mod` | Go module definition |\n| `cmd/omega-api/main.go` | Server entry point, Connect handler registration |\n| `internal/db/db.go` | SQLite read layer — all queries |\n| `internal/handler/orchestrator.go` | OrchestratorService implementation |\n\n### Generated (do not hand-edit)\n| Path | Generated by |\n|------|-------------|\n| `gen/go/omega/v1/` | buf generate → Go protobuf + connect stubs |\n| `dashboard/src/gen/` | buf generate → TypeScript protobuf + connect clients |\n\n### Frontend\n| File | Responsibility |\n|------|---------------|\n| `dashboard/src/client.ts` | Connect transport + service client factory |\n| `dashboard/src/components/layout/Sidebar.tsx` | Nav sidebar |\n| `dashboard/src/components/layout/Header.tsx` | Status bar + orchestrator controls |\n| `dashboard/src/components/NodeCard.tsx` | Node health card with sparkline |\n| `dashboard/src/components/TraceWaterfall.tsx` | SVG waterfall for span trees |\n| `dashboard/src/components/BrainConfigPanel.tsx` | LLM model switcher panel (provider + model selectors) |\n| `dashboard/src/pages/Dashboard.tsx` | Overview page |\n| `dashboard/src/pages/Nodes.tsx` | Node table + detail + BrainConfigPanel |\n| `dashboard/src/pages/Traces.tsx` | Trace list + waterfall |\n| `dashboard/src/pages/Metrics.tsx` | Time-series charts |\n| `dashboard/src/pages/Issues.tsx` | Issues table |\n| `dashboard/src/pages/Memory.tsx` | Memory kernel visualization |\n| `dashboard/src/pages/Convergence.tsx` | Convergence + improvement log |\n| `dashboard/src/App.tsx` | Router + layout wrapper |\n\n---\n\n## Task 1: Proto Definitions\n\n**Files:**\n- Create: `buf.yaml`\n- Create: `buf.gen.yaml`\n- Create: `proto/omega/v1/types.proto`\n- Create: `proto/omega/v1/omega_service.proto`\n- Create: `proto/omega/v1/node_service.proto`\n\n- [ ] **Step 1: Write `buf.yaml`**\n\n```yaml\nversion: v2\nmodules:\n - path: proto\ndeps:\n - buf.build/googleapis/googleapis\nlint:\n use:\n - STANDARD\nbreaking:\n use:\n - FILE\n```\n\n- [ ] **Step 2: Write `buf.gen.yaml`**\n\n```yaml\nversion: v2\nmanaged:\n enabled: true\nplugins:\n - remote: buf.build/protocolbuffers/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/connectrpc/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/bufbuild/es\n out: dashboard/src/gen\n opt:\n - target=ts\n - remote: buf.build/connectrpc/es\n out: dashboard/src/gen\n opt:\n - target=ts\n```\n\n- [ ] **Step 3: Write `proto/omega/v1/types.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage Node {\n string node_id = 1;\n string name = 2;\n string version = 3;\n repeated string capabilities = 4;\n double health = 5;\n string status = 6;\n google.protobuf.Timestamp registered_at = 7;\n google.protobuf.Timestamp last_updated = 8;\n int64 executions_total = 9;\n double error_rate = 10;\n double avg_latency_ms = 11;\n double p95_latency_ms = 12;\n int64 improvement_count = 13;\n ExecutionRecord last_execution = 14;\n}\n\nmessage ExecutionRecord {\n string exec_id = 1;\n string node_id = 2;\n string node_name = 3;\n string trace_id = 4;\n string span_id = 5;\n string action = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n bool success = 10;\n string error_text = 11;\n map metrics = 12;\n int64 cycle = 13;\n}\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage TraceSummary {\n string trace_id = 1;\n google.protobuf.Timestamp trace_started = 2;\n google.protobuf.Timestamp trace_ended = 3;\n double total_duration_ms = 4;\n int64 span_count = 5;\n int64 error_spans = 6;\n int64 cycle = 7;\n}\n\nmessage Span {\n string span_id = 1;\n string trace_id = 2;\n string parent_span_id = 3;\n string node_id = 4;\n string node_name = 5;\n string operation = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n string status = 10;\n int64 cycle = 11;\n}\n\n// ── Issues ─────────────────────────────────────────────────────────────────\n\nmessage Issue {\n string issue_id = 1;\n string detector = 2;\n string severity = 3;\n string description = 4;\n string state = 5;\n google.protobuf.Timestamp opened_at = 6;\n google.protobuf.Timestamp resolved_at = 7;\n int64 cycle_opened = 8;\n int64 cycle_resolved = 9;\n}\n\n// ── Activity ───────────────────────────────────────────────────────────────\n\nmessage ActivityEntry {\n string log_id = 1;\n string action_type = 2;\n string entity_type = 3;\n string entity_id = 4;\n google.protobuf.Timestamp recorded_at = 5;\n int64 cycle = 6;\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────\n\nmessage ImprovementRecord {\n string improve_id = 1;\n string node_id = 2;\n string node_name = 3;\n string from_version = 4;\n string to_version = 5;\n string triggered_by = 6;\n google.protobuf.Timestamp recorded_at = 7;\n int64 cycle = 8;\n map before_metrics = 9;\n map after_metrics = 10;\n}\n\n// ── System health ──────────────────────────────────────────────────────────\n\nmessage SystemHealth {\n string status = 1;\n double composite_score = 2;\n double avg_node_health = 3;\n int64 node_count = 4;\n int64 open_issues = 5;\n int64 error_issues = 6;\n double uptime_seconds = 7;\n int64 total_cycles = 8;\n}\n\n// ── Memory ─────────────────────────────────────────────────────────────────\n\nmessage SemanticConcept {\n string concept = 1;\n string content = 2;\n double confidence = 3;\n int64 evidence_count = 4;\n repeated string tags = 5;\n}\n\nmessage EpisodeEntry {\n string episode_id = 1;\n string event_type = 2;\n google.protobuf.Timestamp timestamp = 3;\n int64 cycle = 4;\n double importance = 5;\n repeated string tags = 6;\n}\n\n// ── Convergence ────────────────────────────────────────────────────────────\n\nmessage ConvergencePoint {\n int64 cycle = 1;\n google.protobuf.Timestamp timestamp = 2;\n double score = 3;\n double pipeline_ms = 4;\n}\n\n// ── Costs ──────────────────────────────────────────────────────────────────\n\nmessage CostEntry {\n string provider = 1;\n string node_id = 2;\n int64 calls = 3;\n double total_ms = 4;\n double total_cost_usd = 5;\n}\n\n// ── Events (for streaming) ─────────────────────────────────────────────────\n\nmessage EventResponse {\n string type = 1;\n oneof payload {\n SystemHealth health_update = 2;\n Node node_update = 3;\n Issue new_issue = 4;\n ImprovementRecord improvement = 5;\n ActivityEntry activity = 6;\n }\n}\n\n// ── LLM Brain config ───────────────────────────────────────────────────────\n\nmessage BrainConfig {\n string provider = 1; // \"anthropic\", \"openai\", \"google\", \"deepseek\", \"ollama\", \"codex\"\n string model = 2; // e.g. \"claude-sonnet-4-6\", \"gpt-4o\", \"gemini-2.0-flash\"\n double temperature = 3;\n int32 max_tokens = 4;\n string system_prompt = 5;\n map extra_config = 6;\n}\n\nmessage NodeConfig {\n string node_id = 1;\n string node_type = 2;\n BrainConfig brain = 3;\n map parameters = 4;\n string version = 5;\n google.protobuf.Timestamp updated_at = 6;\n}\n\nmessage BrainExecutionEntry {\n string exec_id = 1;\n string node_id = 2;\n string provider = 3;\n string model = 4;\n int64 prompt_tokens = 5;\n int64 completion_tokens = 6;\n double latency_ms = 7;\n bool success = 8;\n string error_text = 9;\n google.protobuf.Timestamp executed_at = 10;\n int64 cycle = 11;\n}\n\nmessage ProviderInfo {\n string id = 1; // \"anthropic\"\n string display_name = 2; // \"Anthropic\"\n repeated string models = 3;\n bool requires_api_key = 4;\n bool available = 5;\n}\n```\n\n- [ ] **Step 4: Write `proto/omega/v1/omega_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"omega/v1/types.proto\";\n\nservice OrchestratorService {\n rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);\n rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);\n rpc GetNode(GetNodeRequest) returns (GetNodeResponse);\n rpc ListNodeExecutions(ListNodeExecutionsRequest) returns (ListNodeExecutionsResponse);\n rpc ListTraces(ListTracesRequest) returns (ListTracesResponse);\n rpc GetTrace(GetTraceRequest) returns (GetTraceResponse);\n rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);\n rpc GetMetricsHistory(GetMetricsHistoryRequest) returns (GetMetricsHistoryResponse);\n rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);\n rpc ListActivity(ListActivityRequest) returns (ListActivityResponse);\n rpc ListImprovements(ListImprovementsRequest) returns (ListImprovementsResponse);\n rpc GetNodeMemory(GetNodeMemoryRequest) returns (GetNodeMemoryResponse);\n rpc GetConvergence(GetConvergenceRequest) returns (GetConvergenceResponse);\n rpc GetCosts(GetCostsRequest) returns (GetCostsResponse);\n rpc SubmitFeedback(SubmitFeedbackRequest) returns (SubmitFeedbackResponse);\n rpc StartOrchestrator(StartOrchestratorRequest) returns (StartOrchestratorResponse);\n rpc StopOrchestrator(StopOrchestratorRequest) returns (StopOrchestratorResponse);\n rpc TriggerHeartbeat(TriggerHeartbeatRequest) returns (TriggerHeartbeatResponse);\n rpc StreamEvents(StreamEventsRequest) returns (stream EventResponse);\n\n // ── Brain / LLM config ───────────────────────────────────────────────────\n rpc GetNodeConfig(GetNodeConfigRequest) returns (GetNodeConfigResponse);\n rpc UpdateNodeBrain(UpdateNodeBrainRequest) returns (UpdateNodeBrainResponse);\n rpc ListAvailableProviders(ListAvailableProvidersRequest) returns (ListAvailableProvidersResponse);\n rpc GetBrainExecutionHistory(GetBrainExecutionHistoryRequest) returns (GetBrainExecutionHistoryResponse);\n}\n\n// ── Health ─────────────────────────────────────────────────────────────────\n\nmessage GetHealthRequest {}\nmessage GetHealthResponse { SystemHealth health = 1; }\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage ListNodesRequest {}\nmessage ListNodesResponse { repeated Node nodes = 1; }\n\nmessage GetNodeRequest { string node_id = 1; }\nmessage GetNodeResponse {\n Node node = 1;\n repeated ExecutionRecord recent_executions = 2;\n repeated ImprovementRecord improvements = 3;\n repeated LatencyPoint latency_history = 4;\n}\n\nmessage LatencyPoint {\n google.protobuf.Timestamp ts = 1;\n double duration_ms = 2;\n bool success = 3;\n}\n\nmessage ListNodeExecutionsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListNodeExecutionsResponse { repeated ExecutionRecord executions = 1; }\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage ListTracesRequest { int32 limit = 1; }\nmessage ListTracesResponse { repeated TraceSummary traces = 1; }\n\nmessage GetTraceRequest { string trace_id = 1; }\nmessage GetTraceResponse { repeated Span spans = 1; }\n\n// ── Metrics ─────────────────────────────────────────────────────────────────\n\nmessage GetMetricsRequest {}\nmessage GetMetricsResponse {\n SystemHealth system = 1;\n repeated Node nodes = 2;\n repeated CostEntry costs = 3;\n repeated Issue open_issues = 4;\n repeated TraceSummary recent_traces = 5;\n}\n\nmessage GetMetricsHistoryRequest { string node_id = 1; }\nmessage GetMetricsHistoryResponse {\n repeated NodeLatencyHistory node_histories = 1;\n}\nmessage NodeLatencyHistory {\n string node_id = 1;\n string name = 2;\n repeated LatencyPoint history = 3;\n}\n\n// ── Issues ──────────────────────────────────────────────────────────────────\n\nmessage ListIssuesRequest { string state_filter = 1; }\nmessage ListIssuesResponse { repeated Issue issues = 1; }\n\n// ── Activity ─────────────────────────────────────────────────────────────────\n\nmessage ListActivityRequest { int32 limit = 1; }\nmessage ListActivityResponse { repeated ActivityEntry entries = 1; }\n\n// ── Improvements ─────────────────────────────────────────────────────────────\n\nmessage ListImprovementsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListImprovementsResponse { repeated ImprovementRecord improvements = 1; }\n\n// ── Memory ───────────────────────────────────────────────────────────────────\n\nmessage GetNodeMemoryRequest { string node_id = 1; }\nmessage GetNodeMemoryResponse {\n string namespace = 1;\n int64 episodic_count = 2;\n int64 semantic_count = 3;\n repeated SemanticConcept top_semantic = 4;\n repeated EpisodeEntry recent_episodes = 5;\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nmessage GetConvergenceRequest { int32 limit = 1; }\nmessage GetConvergenceResponse { repeated ConvergencePoint points = 1; }\n\n// ── Costs ────────────────────────────────────────────────────────────────────\n\nmessage GetCostsRequest {}\nmessage GetCostsResponse { repeated CostEntry costs = 1; }\n\n// ── Feedback ─────────────────────────────────────────────────────────────────\n\nmessage SubmitFeedbackRequest {\n string text = 1;\n string target_node = 2;\n}\nmessage SubmitFeedbackResponse { bool ok = 1; }\n\n// ── Orchestrator control ─────────────────────────────────────────────────────\n\nmessage StartOrchestratorRequest { int32 heartbeat_secs = 1; }\nmessage StartOrchestratorResponse { bool started = 1; string message = 2; }\n\nmessage StopOrchestratorRequest {}\nmessage StopOrchestratorResponse { bool stopped = 1; string message = 2; }\n\nmessage TriggerHeartbeatRequest {}\nmessage TriggerHeartbeatResponse { bool triggered = 1; string message = 2; }\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nmessage StreamEventsRequest { int32 poll_interval_ms = 1; }\n\n// ── Brain config ──────────────────────────────────────────────────────────────\n\nmessage GetNodeConfigRequest { string node_id = 1; }\nmessage GetNodeConfigResponse { NodeConfig config = 1; }\n\nmessage UpdateNodeBrainRequest {\n string node_id = 1;\n BrainConfig brain = 2;\n}\nmessage UpdateNodeBrainResponse { NodeConfig config = 1; }\n\nmessage ListAvailableProvidersRequest {}\nmessage ListAvailableProvidersResponse { repeated ProviderInfo providers = 1; }\n\nmessage GetBrainExecutionHistoryRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage GetBrainExecutionHistoryResponse { repeated BrainExecutionEntry entries = 1; }\n```\n\n- [ ] **Step 5: Write `proto/omega/v1/node_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\n// NodeService — contract for inter-node communication.\n// Python nodes implement this; Go orchestrator calls it.\nservice NodeService {\n rpc Execute(ExecuteRequest) returns (ExecuteResponse);\n rpc Evaluate(EvaluateRequest) returns (EvaluateResponse);\n rpc Improve(ImproveRequest) returns (ImproveResponse);\n rpc GetState(GetStateRequest) returns (GetStateResponse);\n rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);\n}\n\nmessage ExecuteRequest {\n string action = 1;\n map parameters = 2;\n map context = 3;\n}\n\nmessage ExecuteResponse {\n bool success = 1;\n bytes result_json = 2;\n repeated string errors = 3;\n map metrics = 4;\n}\n\nmessage EvaluateRequest {}\nmessage EvaluateResponse { map metrics = 1; }\n\nmessage ImproveRequest { map feedback = 1; }\nmessage ImproveResponse { bool changed = 1; string new_version = 2; }\n\nmessage GetStateRequest {}\nmessage GetStateResponse {\n string node_id = 1;\n string name = 2;\n string version = 3;\n double health = 4;\n string status = 5;\n repeated string capabilities = 6;\n}\n\nmessage GetCapabilitiesRequest {}\nmessage GetCapabilitiesResponse { repeated string capabilities = 1; }\n```\n\n- [ ] **Step 6: Lint and validate protos**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf dep update\nbuf lint\n```\n\nExpected: no errors.\n\n- [ ] **Step 7: Commit protos and buf config**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add buf.yaml buf.gen.yaml proto/\ngit commit -m \"feat(proto): add Omega protobuf service definitions\"\n```\n\n---\n\n## Task 2: Go Module + Code Generation\n\n**Files:**\n- Create: `go.mod`\n- Create: `go.sum` (generated)\n- Create: `gen/go/` (generated)\n- Create: `dashboard/src/gen/` (generated)\n\n- [ ] **Step 1: Initialize Go module**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo mod init github.com/benebsworth/omega\n```\n\n- [ ] **Step 2: Add Go dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo get connectrpc.com/connect@latest\ngo get google.golang.org/protobuf@latest\ngo get modernc.org/sqlite@latest\ngo get golang.org/x/net@latest\n```\n\n- [ ] **Step 3: Run buf generate**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf generate\n```\n\nExpected: creates `gen/go/omega/v1/*.pb.go` and `gen/go/omega/v1/*connect*.go`, and `dashboard/src/gen/` with TypeScript files.\n\n- [ ] **Step 4: Verify generated Go compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./gen/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 5: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add go.mod go.sum gen/ dashboard/src/gen/\ngit commit -m \"feat(gen): add Go module and run buf generate for Go + TS stubs\"\n```\n\n---\n\n## Task 3: Go Database Layer\n\n**Files:**\n- Create: `internal/db/db.go`\n\n- [ ] **Step 1: Write `internal/db/db.go`**\n\n```go\n// Package db provides read-only access to the Omega SQLite state and memory databases.\n// The Python node layer writes; this Go layer reads.\npackage db\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t_ \"modernc.org/sqlite\"\n)\n\nconst (\n\tStateDBPath = \"/tmp/omega_vectora_state.db\"\n\tMemoryDBPath = \"/tmp/omega_vectora_memory.db\"\n)\n\n// DB holds connections to both Omega SQLite databases.\ntype DB struct {\n\tstate *sql.DB\n\tmemory *sql.DB\n}\n\n// New opens both databases in WAL mode for concurrent reads.\nfunc New(stateDBPath, memoryDBPath string) (*DB, error) {\n\tstate, err := openDB(stateDBPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open state db: %w\", err)\n\t}\n\tmemory, err := openDB(memoryDBPath)\n\tif err != nil {\n\t\tstate.Close()\n\t\treturn nil, fmt.Errorf(\"open memory db: %w\", err)\n\t}\n\td := &DB{state: state, memory: memory}\n\tif err := d.ensureBrainTables(); err != nil {\n\t\tstate.Close()\n\t\tmemory.Close()\n\t\treturn nil, fmt.Errorf(\"ensure brain tables: %w\", err)\n\t}\n\treturn d, nil\n}\n\nfunc openDB(path string) (*sql.DB, error) {\n\tdb, err := sql.Open(\"sqlite\", path+\"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdb.SetMaxOpenConns(1) // SQLite WAL: one writer, many readers via connection pooling\n\tdb.SetMaxIdleConns(1)\n\treturn db, db.Ping()\n}\n\nfunc (d *DB) Close() {\n\td.state.Close()\n\td.memory.Close()\n}\n\n// ── Node types ──────────────────────────────────────────────────────────────\n\ntype Node struct {\n\tNodeID string\n\tName string\n\tVersion string\n\tCapabilities []string\n\tHealth float64\n\tStatus string\n\tRegisteredAt float64\n\tLastUpdated float64\n\tExecutionsTotal int64\n\tErrorRate float64\n\tAvgLatencyMS float64\n\tP95LatencyMS float64\n\tImprovementCount int64\n\tLastExecution *Execution\n}\n\ntype Execution struct {\n\tExecID string\n\tNodeID string\n\tNodeName string\n\tTraceID string\n\tSpanID string\n\tAction string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tSuccess bool\n\tErrorText string\n\tMetrics map[string]float64\n\tCycle int64\n}\n\ntype LatencyPoint struct {\n\tStartedAt float64\n\tDurationMS float64\n\tSuccess bool\n}\n\n// ── Trace types ─────────────────────────────────────────────────────────────\n\ntype TraceSummary struct {\n\tTraceID string\n\tTraceStarted float64\n\tTraceEnded *float64\n\tTotalDurationMS float64\n\tSpanCount int64\n\tErrorSpans int64\n\tCycle int64\n}\n\ntype Span struct {\n\tSpanID string\n\tTraceID string\n\tParentSpanID string\n\tNodeID string\n\tNodeName string\n\tOperation string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tStatus string\n\tCycle int64\n}\n\n// ── Issue types ──────────────────────────────────────────────────────────────\n\ntype Issue struct {\n\tIssueID string\n\tDetector string\n\tSeverity string\n\tDescription string\n\tState string\n\tOpenedAt float64\n\tResolvedAt *float64\n\tCycleOpened int64\n\tCycleResolved *int64\n}\n\n// ── Activity types ────────────────────────────────────────────────────────────\n\ntype ActivityEntry struct {\n\tLogID string\n\tActionType string\n\tEntityType string\n\tEntityID string\n\tRecordedAt float64\n\tCycle int64\n}\n\n// ── Improvement types ─────────────────────────────────────────────────────────\n\ntype Improvement struct {\n\tImproveID string\n\tNodeID string\n\tNodeName string\n\tFromVersion string\n\tToVersion string\n\tTriggeredBy string\n\tRecordedAt float64\n\tCycle int64\n\tBeforeMetrics map[string]float64\n\tAfterMetrics map[string]float64\n}\n\n// ── Cost types ────────────────────────────────────────────────────────────────\n\ntype CostEntry struct {\n\tProvider string\n\tNodeID string\n\tCalls int64\n\tTotalMS float64\n\tTotalCostUSD float64\n}\n\n// ── Memory types ──────────────────────────────────────────────────────────────\n\ntype SemanticConcept struct {\n\tConcept string\n\tContent string\n\tConfidence float64\n\tEvidenceCount int64\n\tTags []string\n}\n\ntype EpisodeEntry struct {\n\tEpisodeID string\n\tEventType string\n\tTimestamp float64\n\tCycle int64\n\tImportance float64\n\tTags []string\n}\n\ntype ConvergencePoint struct {\n\tCycle int64\n\tTimestamp float64\n\tScore float64\n\tPipelineMS float64\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\ntype SystemHealth struct {\n\tStatus string\n\tCompositeScore float64\n\tAvgNodeHealth float64\n\tNodeCount int64\n\tOpenIssues int64\n\tErrorIssues int64\n\tUptimeSeconds float64\n\tTotalCycles int64\n}\n\n// ── Query methods ──────────────────────────────────────────────────────────────\n\n// AllNodes returns all registered nodes with computed metrics.\nfunc (d *DB) AllNodes() ([]*Node, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes ORDER BY registered_at`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar nodes []*Node\n\tfor rows.Next() {\n\t\tn := &Node{}\n\t\tvar capsJSON string\n\t\tif err := rows.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\t\tnodes = append(nodes, n)\n\t}\n\n\t// Enrich with computed metrics\n\tfor _, n := range nodes {\n\t\tif err := d.enrichNode(n); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn nodes, nil\n}\n\n// GetNode returns one node by ID, enriched with metrics.\nfunc (d *DB) GetNode(nodeID string) (*Node, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes WHERE node_id = ?`, nodeID)\n\tn := &Node{}\n\tvar capsJSON string\n\tif err := row.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\treturn n, d.enrichNode(n)\n}\n\nfunc (d *DB) enrichNode(n *Node) error {\n\t// Execution stats\n\trow := d.state.QueryRow(`\n\t\tSELECT COUNT(*),\n\t\t SUM(CASE WHEN success=0 THEN 1 ELSE 0 END),\n\t\t AVG(duration_ms),\n\t\t MAX(started_at)\n\t\tFROM node_executions WHERE node_id = ?`, n.NodeID)\n\tvar total, failed int64\n\tvar avgMS sql.NullFloat64\n\tvar lastStarted sql.NullFloat64\n\trow.Scan(&total, &failed, &avgMS, &lastStarted) //nolint:errcheck\n\n\tn.ExecutionsTotal = total\n\tif total > 0 {\n\t\tn.ErrorRate = float64(failed) / float64(total)\n\t}\n\tif avgMS.Valid {\n\t\tn.AvgLatencyMS = avgMS.Float64\n\t}\n\n\t// p95 latency — two-query approach (SQLite rejects COUNT(*) inside OFFSET)\n\tvar p95Count int64\n\td.state.QueryRow(`SELECT COUNT(*) FROM node_executions WHERE node_id = ? AND duration_ms IS NOT NULL`, n.NodeID).Scan(&p95Count) //nolint:errcheck\n\tif p95Count > 0 {\n\t\toffset := int64(float64(p95Count) * 0.95)\n\t\tp95row := d.state.QueryRow(`\n\t\t\tSELECT duration_ms FROM node_executions\n\t\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\t\tORDER BY duration_ms\n\t\t\tLIMIT 1 OFFSET ?\n\t\t`, n.NodeID, offset)\n\t\tvar p95 sql.NullFloat64\n\t\tp95row.Scan(&p95) //nolint:errcheck\n\t\tif p95.Valid {\n\t\t\tn.P95LatencyMS = p95.Float64\n\t\t}\n\t}\n\n\t// Improvement count\n\trow2 := d.state.QueryRow(\n\t\t`SELECT COUNT(*) FROM improvement_log WHERE node_id = ?`, n.NodeID)\n\trow2.Scan(&n.ImprovementCount) //nolint:errcheck\n\n\t// Last execution\n\tlastExec, err := d.lastExecution(n.NodeID)\n\tif err == nil {\n\t\tn.LastExecution = lastExec\n\t}\n\treturn nil\n}\n\nfunc (d *DB) lastExecution(nodeID string) (*Execution, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\t action, started_at, ended_at, duration_ms, success,\n\t\t COALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE node_id = ?\n\t\tORDER BY started_at DESC LIMIT 1`, nodeID)\n\treturn scanExecution(row)\n}\n\nfunc scanExecution(row *sql.Row) (*Execution, error) {\n\te := &Execution{}\n\tvar metricsJSON string\n\tvar success int\n\tvar endedAt, durationMS sql.NullFloat64\n\terr := row.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t&e.ErrorText, &metricsJSON, &e.Cycle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\te.Success = success == 1\n\tif endedAt.Valid {\n\t\tv := endedAt.Float64\n\t\te.EndedAt = &v\n\t}\n\tif durationMS.Valid {\n\t\tv := durationMS.Float64\n\t\te.DurationMS = &v\n\t}\n\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\treturn e, nil\n}\n\n// GetExecutions returns recent executions, optionally filtered by node.\nfunc (d *DB) GetExecutions(nodeID string, limit int) ([]*Execution, error) {\n\tquery := `SELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\taction, started_at, ended_at, duration_ms, success,\n\t\tCOALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY started_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\treturn scanExecutions(rows)\n}\n\nfunc scanExecutions(rows *sql.Rows) ([]*Execution, error) {\n\tvar result []*Execution\n\tfor rows.Next() {\n\t\te := &Execution{}\n\t\tvar metricsJSON string\n\t\tvar success int\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t\t&e.ErrorText, &metricsJSON, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\te.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\te.DurationMS = &v\n\t\t}\n\t\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\t\tresult = append(result, e)\n\t}\n\treturn result, nil\n}\n\n// LatencyHistory returns (started_at, duration_ms, success) for charting.\nfunc (d *DB) LatencyHistory(nodeID string, limit int) ([]*LatencyPoint, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT started_at, duration_ms, success\n\t\tFROM node_executions\n\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\tORDER BY started_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*LatencyPoint\n\tfor rows.Next() {\n\t\tp := &LatencyPoint{}\n\t\tvar success int\n\t\tif err := rows.Scan(&p.StartedAt, &p.DurationMS, &success); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.Success = success == 1\n\t\tpoints = append(points, p)\n\t}\n\t// Reverse to chronological order\n\tfor i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {\n\t\tpoints[i], points[j] = points[j], points[i]\n\t}\n\treturn points, nil\n}\n\n// RecentTraces returns recent trace summaries.\nfunc (d *DB) RecentTraces(limit int) ([]*TraceSummary, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT trace_id,\n\t\t MIN(started_at) as trace_started,\n\t\t MAX(ended_at) as trace_ended,\n\t\t (MAX(ended_at) - MIN(started_at)) * 1000 as total_duration_ms,\n\t\t COUNT(*) as span_count,\n\t\t SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_spans,\n\t\t MAX(cycle) as cycle\n\t\tFROM traces\n\t\tGROUP BY trace_id\n\t\tORDER BY trace_started DESC\n\t\tLIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar summaries []*TraceSummary\n\tfor rows.Next() {\n\t\tt := &TraceSummary{}\n\t\tvar traceEnded sql.NullFloat64\n\t\tif err := rows.Scan(&t.TraceID, &t.TraceStarted, &traceEnded,\n\t\t\t&t.TotalDurationMS, &t.SpanCount, &t.ErrorSpans, &t.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif traceEnded.Valid {\n\t\t\tv := traceEnded.Float64\n\t\t\tt.TraceEnded = &v\n\t\t}\n\t\tsummaries = append(summaries, t)\n\t}\n\treturn summaries, nil\n}\n\n// GetTraceSpans returns all spans for a trace.\nfunc (d *DB) GetTraceSpans(traceID string) ([]*Span, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT span_id, trace_id, COALESCE(parent_span_id,''),\n\t\t COALESCE(node_id,''), COALESCE(node_name,''),\n\t\t operation, started_at, ended_at, duration_ms, status, cycle\n\t\tFROM traces WHERE trace_id = ? ORDER BY started_at`, traceID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar spans []*Span\n\tfor rows.Next() {\n\t\ts := &Span{}\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&s.SpanID, &s.TraceID, &s.ParentSpanID,\n\t\t\t&s.NodeID, &s.NodeName, &s.Operation,\n\t\t\t&s.StartedAt, &endedAt, &durationMS, &s.Status, &s.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\ts.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\ts.DurationMS = &v\n\t\t}\n\t\tspans = append(spans, s)\n\t}\n\treturn spans, nil\n}\n\n// GetIssues returns issues, optionally filtered by state.\nfunc (d *DB) GetIssues(stateFilter string) ([]*Issue, error) {\n\tquery := `SELECT issue_id, detector, severity, description, state,\n\t\topened_at, resolved_at, cycle_opened, cycle_resolved\n\t\tFROM issues WHERE 1=1`\n\targs := []any{}\n\tif stateFilter != \"\" && stateFilter != \"all\" {\n\t\tif stateFilter == \"open\" {\n\t\t\tquery += \" AND state != 'resolved'\"\n\t\t} else {\n\t\t\tquery += \" AND state = ?\"\n\t\t\targs = append(args, stateFilter)\n\t\t}\n\t}\n\tquery += \" ORDER BY opened_at DESC LIMIT 100\"\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar issues []*Issue\n\tfor rows.Next() {\n\t\ti := &Issue{}\n\t\tif err := rows.Scan(&i.IssueID, &i.Detector, &i.Severity, &i.Description,\n\t\t\t&i.State, &i.OpenedAt, &i.ResolvedAt, &i.CycleOpened, &i.CycleResolved); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tissues = append(issues, i)\n\t}\n\treturn issues, nil\n}\n\n// RecentActivity returns recent activity log entries.\nfunc (d *DB) RecentActivity(limit int) ([]*ActivityEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT log_id, action_type, entity_type, entity_id, recorded_at, cycle\n\t\tFROM activity_log ORDER BY recorded_at DESC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*ActivityEntry\n\tfor rows.Next() {\n\t\te := &ActivityEntry{}\n\t\tif err := rows.Scan(&e.LogID, &e.ActionType, &e.EntityType,\n\t\t\t&e.EntityID, &e.RecordedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n\n// GetImprovements returns improvement log, optionally by node.\nfunc (d *DB) GetImprovements(nodeID string, limit int) ([]*Improvement, error) {\n\tquery := `SELECT improve_id, node_id, node_name, from_version, to_version,\n\t\ttriggered_by, recorded_at, cycle, before_metrics_json, after_metrics_json\n\t\tFROM improvement_log WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY recorded_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar imps []*Improvement\n\tfor rows.Next() {\n\t\timp := &Improvement{}\n\t\tvar beforeJSON, afterJSON string\n\t\tif err := rows.Scan(&imp.ImproveID, &imp.NodeID, &imp.NodeName,\n\t\t\t&imp.FromVersion, &imp.ToVersion, &imp.TriggeredBy,\n\t\t\t&imp.RecordedAt, &imp.Cycle, &beforeJSON, &afterJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(beforeJSON), &imp.BeforeMetrics) //nolint:errcheck\n\t\tjson.Unmarshal([]byte(afterJSON), &imp.AfterMetrics) //nolint:errcheck\n\t\timps = append(imps, imp)\n\t}\n\treturn imps, nil\n}\n\n// GetCosts returns aggregated cost by provider and node.\nfunc (d *DB) GetCosts() ([]*CostEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT provider, node_id,\n\t\t COUNT(*) as calls,\n\t\t SUM(duration_ms) as total_ms,\n\t\t SUM(estimated_cost_usd) as total_cost\n\t\tFROM cost_events GROUP BY provider, node_id`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar costs []*CostEntry\n\tfor rows.Next() {\n\t\tc := &CostEntry{}\n\t\tif err := rows.Scan(&c.Provider, &c.NodeID, &c.Calls, &c.TotalMS, &c.TotalCostUSD); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcosts = append(costs, c)\n\t}\n\treturn costs, nil\n}\n\n// GetConvergence reads cycle_summary episodes from the memory DB.\nfunc (d *DB) GetConvergence(limit int) ([]*ConvergencePoint, error) {\n\trows, err := d.memory.Query(`\n\t\tSELECT cycle, timestamp, content_json\n\t\tFROM episodes WHERE event_type = 'cycle_summary'\n\t\tORDER BY cycle ASC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*ConvergencePoint\n\tfor rows.Next() {\n\t\tp := &ConvergencePoint{}\n\t\tvar contentJSON string\n\t\tif err := rows.Scan(&p.Cycle, &p.Timestamp, &contentJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar content map[string]any\n\t\tif err := json.Unmarshal([]byte(contentJSON), &content); err == nil {\n\t\t\tif s, ok := content[\"score\"].(float64); ok {\n\t\t\t\tp.Score = s\n\t\t\t}\n\t\t\tif ms, ok := content[\"pipeline_ms\"].(float64); ok {\n\t\t\t\tp.PipelineMS = ms\n\t\t\t}\n\t\t}\n\t\tpoints = append(points, p)\n\t}\n\treturn points, nil\n}\n\n// GetMemoryStats returns episodic + semantic counts for a namespace.\nfunc (d *DB) GetMemoryStats(namespace string) (int64, int64, []*SemanticConcept, []*EpisodeEntry, error) {\n\tvar epCount, semCount int64\n\n\t// Episodic count\n\tquery := \"SELECT COUNT(*) FROM episodes\"\n\targs := []any{}\n\tif namespace != \"\" {\n\t\tquery += \" WHERE namespace = ?\"\n\t\targs = append(args, namespace)\n\t}\n\td.memory.QueryRow(query, args...).Scan(&epCount) //nolint:errcheck\n\n\t// Semantic count\n\tsemQuery := \"SELECT COUNT(*) FROM semantic_memories\"\n\tsemArgs := []any{}\n\tif namespace != \"\" {\n\t\tsemQuery += \" WHERE namespace = ?\"\n\t\tsemArgs = append(semArgs, namespace)\n\t}\n\td.memory.QueryRow(semQuery, semArgs...).Scan(&semCount) //nolint:errcheck\n\n\t// Top semantic\n\tsemRowQuery := \"SELECT concept, content, confidence, evidence_count, tags_json FROM semantic_memories\"\n\tif namespace != \"\" {\n\t\tsemRowQuery += \" WHERE namespace = ?\"\n\t}\n\tsemRowQuery += \" ORDER BY confidence DESC LIMIT 20\"\n\tsemRows, err := d.memory.Query(semRowQuery, semArgs...)\n\tif err != nil {\n\t\treturn epCount, semCount, nil, nil, err\n\t}\n\tdefer semRows.Close()\n\tvar concepts []*SemanticConcept\n\tfor semRows.Next() {\n\t\tc := &SemanticConcept{}\n\t\tvar tagsJSON string\n\t\tif err := semRows.Scan(&c.Concept, &c.Content, &c.Confidence, &c.EvidenceCount, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, nil, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &c.Tags) //nolint:errcheck\n\t\tconcepts = append(concepts, c)\n\t}\n\n\t// Recent episodes\n\tepRowQuery := \"SELECT episode_id, event_type, timestamp, cycle, importance, tags_json FROM episodes\"\n\tif namespace != \"\" {\n\t\tepRowQuery += \" WHERE namespace = ?\"\n\t}\n\tepRowQuery += \" ORDER BY timestamp DESC LIMIT 20\"\n\tepRows, err := d.memory.Query(epRowQuery, args...)\n\tif err != nil {\n\t\treturn epCount, semCount, concepts, nil, err\n\t}\n\tdefer epRows.Close()\n\tvar episodes []*EpisodeEntry\n\tfor epRows.Next() {\n\t\te := &EpisodeEntry{}\n\t\tvar tagsJSON string\n\t\tif err := epRows.Scan(&e.EpisodeID, &e.EventType, &e.Timestamp, &e.Cycle, &e.Importance, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, concepts, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &e.Tags) //nolint:errcheck\n\t\tepisodes = append(episodes, e)\n\t}\n\n\treturn epCount, semCount, concepts, episodes, nil\n}\n\n// SystemHealth computes overall system health from node states.\nfunc (d *DB) SystemHealth() (*SystemHealth, error) {\n\tnodes, err := d.AllNodes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn &SystemHealth{Status: \"no_nodes\"}, nil\n\t}\n\n\tvar totalHealth float64\n\tvar minHealth float64 = 1.0\n\tfor _, n := range nodes {\n\t\ttotalHealth += n.Health\n\t\tif n.Health < minHealth {\n\t\t\tminHealth = n.Health\n\t\t}\n\t}\n\tavgHealth := totalHealth / float64(len(nodes))\n\n\tissues, _ := d.GetIssues(\"open\")\n\topenCount := int64(len(issues))\n\tvar errorIssues int64\n\tfor _, i := range issues {\n\t\tif i.Severity == \"error\" {\n\t\t\terrorIssues++\n\t\t}\n\t}\n\n\tissuePenalty := float64(errorIssues) * 0.1\n\tif issuePenalty > 0.3 {\n\t\tissuePenalty = 0.3\n\t}\n\tcomposite := avgHealth - issuePenalty\n\tif composite < 0 {\n\t\tcomposite = 0\n\t}\n\n\tstatus := \"healthy\"\n\tif composite < 0.6 {\n\t\tstatus = \"critical\"\n\t} else if composite < 0.8 {\n\t\tstatus = \"degraded\"\n\t}\n\n\t// Uptime from first registered_at\n\tvar minRegAt float64 = float64(time.Now().Unix())\n\tfor _, n := range nodes {\n\t\tif n.RegisteredAt < minRegAt {\n\t\t\tminRegAt = n.RegisteredAt\n\t\t}\n\t}\n\tuptime := float64(time.Now().Unix()) - minRegAt\n\n\t// Total cycles\n\tvar totalCycles int64\n\td.state.QueryRow(\"SELECT COALESCE(MAX(cycle), 0) FROM node_executions\").Scan(&totalCycles) //nolint:errcheck\n\n\treturn &SystemHealth{\n\t\tStatus: status,\n\t\tCompositeScore: round3(composite),\n\t\tAvgNodeHealth: round3(avgHealth),\n\t\tNodeCount: int64(len(nodes)),\n\t\tOpenIssues: openCount,\n\t\tErrorIssues: errorIssues,\n\t\tUptimeSeconds: uptime,\n\t\tTotalCycles: totalCycles,\n\t}, nil\n}\n\nfunc round3(f float64) float64 {\n\treturn float64(int(f*1000+0.5)) / 1000\n}\n\n// ── Brain config types ─────────────────────────────────────────────────────────\n\n// BrainConfig is the LLM adapter config for a node — stored in Go-managed tables.\ntype BrainConfig struct {\n\tNodeID string\n\tProvider string\n\tModel string\n\tTemperature float64\n\tMaxTokens int64\n\tSystemPrompt string\n\tExtraConfig map[string]string\n\tUpdatedAt float64\n}\n\ntype BrainExecutionEntry struct {\n\tExecID string\n\tNodeID string\n\tProvider string\n\tModel string\n\tPromptTokens int64\n\tCompletionTokens int64\n\tLatencyMS float64\n\tSuccess bool\n\tErrorText string\n\tExecutedAt float64\n\tCycle int64\n}\n\n// ── Brain config DB methods ────────────────────────────────────────────────────\n\n// ensureBrainTables creates the Go-managed brain config tables if they don't exist.\n// Call this in New() before returning.\nfunc (d *DB) ensureBrainTables() error {\n\t_, err := d.state.Exec(`\n\t\tCREATE TABLE IF NOT EXISTS node_brain_config (\n\t\t\tnode_id TEXT PRIMARY KEY,\n\t\t\tprovider TEXT NOT NULL DEFAULT 'anthropic',\n\t\t\tmodel TEXT NOT NULL DEFAULT 'claude-sonnet-4-6',\n\t\t\ttemperature REAL NOT NULL DEFAULT 0.7,\n\t\t\tmax_tokens INTEGER NOT NULL DEFAULT 4096,\n\t\t\tsystem_prompt TEXT NOT NULL DEFAULT '',\n\t\t\textra_config TEXT NOT NULL DEFAULT '{}',\n\t\t\tupdated_at REAL NOT NULL DEFAULT (unixepoch('now'))\n\t\t);\n\t\tCREATE TABLE IF NOT EXISTS brain_execution_log (\n\t\t\texec_id TEXT PRIMARY KEY,\n\t\t\tnode_id TEXT NOT NULL,\n\t\t\tprovider TEXT NOT NULL,\n\t\t\tmodel TEXT NOT NULL,\n\t\t\tprompt_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tcompletion_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tlatency_ms REAL NOT NULL DEFAULT 0,\n\t\t\tsuccess INTEGER NOT NULL DEFAULT 1,\n\t\t\terror_text TEXT NOT NULL DEFAULT '',\n\t\t\texecuted_at REAL NOT NULL DEFAULT (unixepoch('now')),\n\t\t\tcycle INTEGER NOT NULL DEFAULT 0\n\t\t);`)\n\treturn err\n}\n\n// GetBrainConfig returns the brain config for a node (defaults if not set).\nfunc (d *DB) GetBrainConfig(nodeID string) (*BrainConfig, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, provider, model, temperature, max_tokens,\n\t\t system_prompt, extra_config, updated_at\n\t\tFROM node_brain_config WHERE node_id = ?`, nodeID)\n\tc := &BrainConfig{}\n\tvar extraJSON string\n\terr := row.Scan(&c.NodeID, &c.Provider, &c.Model, &c.Temperature,\n\t\t&c.MaxTokens, &c.SystemPrompt, &extraJSON, &c.UpdatedAt)\n\tif err == sql.ErrNoRows {\n\t\t// Return defaults\n\t\treturn &BrainConfig{\n\t\t\tNodeID: nodeID,\n\t\t\tProvider: \"anthropic\",\n\t\t\tModel: \"claude-sonnet-4-6\",\n\t\t\tTemperature: 0.7,\n\t\t\tMaxTokens: 4096,\n\t\t}, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(extraJSON), &c.ExtraConfig) //nolint:errcheck\n\treturn c, nil\n}\n\n// SetBrainConfig upserts the brain config for a node.\nfunc (d *DB) SetBrainConfig(c *BrainConfig) error {\n\textraJSON, _ := json.Marshal(c.ExtraConfig)\n\t_, err := d.state.Exec(`\n\t\tINSERT INTO node_brain_config\n\t\t\t(node_id, provider, model, temperature, max_tokens, system_prompt, extra_config, updated_at)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, unixepoch('now'))\n\t\tON CONFLICT(node_id) DO UPDATE SET\n\t\t\tprovider=excluded.provider, model=excluded.model,\n\t\t\ttemperature=excluded.temperature, max_tokens=excluded.max_tokens,\n\t\t\tsystem_prompt=excluded.system_prompt, extra_config=excluded.extra_config,\n\t\t\tupdated_at=excluded.updated_at`,\n\t\tc.NodeID, c.Provider, c.Model, c.Temperature,\n\t\tc.MaxTokens, c.SystemPrompt, string(extraJSON))\n\treturn err\n}\n\n// GetBrainHistory returns recent brain execution log for a node.\nfunc (d *DB) GetBrainHistory(nodeID string, limit int) ([]*BrainExecutionEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT exec_id, node_id, provider, model, prompt_tokens, completion_tokens,\n\t\t latency_ms, success, error_text, executed_at, cycle\n\t\tFROM brain_execution_log WHERE node_id = ?\n\t\tORDER BY executed_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*BrainExecutionEntry\n\tfor rows.Next() {\n\t\te := &BrainExecutionEntry{}\n\t\tvar success int\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.Provider, &e.Model,\n\t\t\t&e.PromptTokens, &e.CompletionTokens, &e.LatencyMS,\n\t\t\t&success, &e.ErrorText, &e.ExecutedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/db/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/ go.mod go.sum\ngit commit -m \"feat(db): add Go SQLite read layer for Omega state + memory DBs\"\n```\n\n---\n\n## Task 4: Go Connect-RPC Handler\n\n**Files:**\n- Create: `internal/handler/orchestrator.go`\n\n- [ ] **Step 1: Write `internal/handler/orchestrator.go`**\n\n```go\n// Package handler implements the OrchestratorService Connect-RPC handler.\npackage handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n)\n\n// Ensure interface satisfaction at compile time.\n// Buf's connectrpc/go plugin generates the handler interface in the same package as the proto types.\nvar _ omegav1.OrchestratorServiceHandler = (*OrchestratorHandler)(nil)\n\n// OrchestratorHandler implements OrchestratorService.\ntype OrchestratorHandler struct {\n\tdb *db.DB\n\tmu sync.Mutex\n\torchCmd *exec.Cmd\n\torchRunning bool\n}\n\n// New creates an OrchestratorHandler backed by the given DB.\nfunc New(database *db.DB) *OrchestratorHandler {\n\treturn &OrchestratorHandler{db: database}\n}\n\n// tsFromUnix converts a float64 Unix timestamp to a protobuf Timestamp.\nfunc tsFromUnix(u float64) *timestamppb.Timestamp {\n\tif u == 0 {\n\t\treturn nil\n\t}\n\tsec := int64(u)\n\tnsec := int32((u - float64(sec)) * 1e9)\n\treturn timestamppb.New(time.Unix(sec, int64(nsec)))\n}\n\nfunc tsFromUnixPtr(u *float64) *timestamppb.Timestamp {\n\tif u == nil {\n\t\treturn nil\n\t}\n\treturn tsFromUnix(*u)\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetHealth(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetHealthRequest],\n) (*connect.Response[omegav1.GetHealthResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetHealthResponse{\n\t\tHealth: dbHealthToProto(health),\n\t}), nil\n}\n\nfunc dbHealthToProto(h *db.SystemHealth) *omegav1.SystemHealth {\n\treturn &omegav1.SystemHealth{\n\t\tStatus: h.Status,\n\t\tCompositeScore: h.CompositeScore,\n\t\tAvgNodeHealth: h.AvgNodeHealth,\n\t\tNodeCount: h.NodeCount,\n\t\tOpenIssues: h.OpenIssues,\n\t\tErrorIssues: h.ErrorIssues,\n\t\tUptimeSeconds: h.UptimeSeconds,\n\t\tTotalCycles: h.TotalCycles,\n\t}\n}\n\n// ── Nodes ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListNodes(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodesRequest],\n) (*connect.Response[omegav1.ListNodesResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoNodes []*omegav1.Node\n\tfor _, n := range nodes {\n\t\tprotoNodes = append(protoNodes, dbNodeToProto(n))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodesResponse{Nodes: protoNodes}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNode(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeRequest],\n) (*connect.Response[omegav1.GetNodeResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\texecs, _ := h.db.GetExecutions(req.Msg.NodeId, 10)\n\timps, _ := h.db.GetImprovements(req.Msg.NodeId, 20)\n\thistory, _ := h.db.LatencyHistory(req.Msg.NodeId, 100)\n\n\tresp := &omegav1.GetNodeResponse{\n\t\tNode: dbNodeToProto(n),\n\t\tImprovements: dbImpsToProto(imps),\n\t}\n\tfor _, e := range execs {\n\t\tresp.RecentExecutions = append(resp.RecentExecutions, dbExecToProto(e))\n\t}\n\tfor _, p := range history {\n\t\tresp.LatencyHistory = append(resp.LatencyHistory, &omegav1.LatencyPoint{\n\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\tDurationMs: p.DurationMS,\n\t\t\tSuccess: p.Success,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) ListNodeExecutions(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodeExecutionsRequest],\n) (*connect.Response[omegav1.ListNodeExecutionsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\texecs, err := h.db.GetExecutions(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoExecs []*omegav1.ExecutionRecord\n\tfor _, e := range execs {\n\t\tprotoExecs = append(protoExecs, dbExecToProto(e))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodeExecutionsResponse{Executions: protoExecs}), nil\n}\n\nfunc dbNodeToProto(n *db.Node) *omegav1.Node {\n\tnode := &omegav1.Node{\n\t\tNodeId: n.NodeID,\n\t\tName: n.Name,\n\t\tVersion: n.Version,\n\t\tCapabilities: n.Capabilities,\n\t\tHealth: n.Health,\n\t\tStatus: n.Status,\n\t\tRegisteredAt: tsFromUnix(n.RegisteredAt),\n\t\tLastUpdated: tsFromUnix(n.LastUpdated),\n\t\tExecutionsTotal: n.ExecutionsTotal,\n\t\tErrorRate: n.ErrorRate,\n\t\tAvgLatencyMs: n.AvgLatencyMS,\n\t\tP95LatencyMs: n.P95LatencyMS,\n\t\tImprovementCount: n.ImprovementCount,\n\t}\n\tif n.LastExecution != nil {\n\t\tnode.LastExecution = dbExecToProto(n.LastExecution)\n\t}\n\treturn node\n}\n\nfunc dbExecToProto(e *db.Execution) *omegav1.ExecutionRecord {\n\trec := &omegav1.ExecutionRecord{\n\t\tExecId: e.ExecID,\n\t\tNodeId: e.NodeID,\n\t\tNodeName: e.NodeName,\n\t\tTraceId: e.TraceID,\n\t\tSpanId: e.SpanID,\n\t\tAction: e.Action,\n\t\tStartedAt: tsFromUnix(e.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(e.EndedAt),\n\t\tSuccess: e.Success,\n\t\tErrorText: e.ErrorText,\n\t\tMetrics: e.Metrics,\n\t\tCycle: e.Cycle,\n\t}\n\tif e.DurationMS != nil {\n\t\trec.DurationMs = *e.DurationMS\n\t}\n\treturn rec\n}\n\n// ── Traces ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListTraces(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListTracesRequest],\n) (*connect.Response[omegav1.ListTracesResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 20\n\t}\n\ttraces, err := h.db.RecentTraces(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoTraces []*omegav1.TraceSummary\n\tfor _, t := range traces {\n\t\tprotoTraces = append(protoTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(&omegav1.ListTracesResponse{Traces: protoTraces}), nil\n}\n\nfunc (h *OrchestratorHandler) GetTrace(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetTraceRequest],\n) (*connect.Response[omegav1.GetTraceResponse], error) {\n\tspans, err := h.db.GetTraceSpans(req.Msg.TraceId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tif len(spans) == 0 {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"trace not found: %s\", req.Msg.TraceId))\n\t}\n\tvar protoSpans []*omegav1.Span\n\tfor _, s := range spans {\n\t\tprotoSpans = append(protoSpans, dbSpanToProto(s))\n\t}\n\treturn connect.NewResponse(&omegav1.GetTraceResponse{Spans: protoSpans}), nil\n}\n\nfunc dbTraceSummaryToProto(t *db.TraceSummary) *omegav1.TraceSummary {\n\treturn &omegav1.TraceSummary{\n\t\tTraceId: t.TraceID,\n\t\tTraceStarted: tsFromUnix(t.TraceStarted),\n\t\tTraceEnded: tsFromUnixPtr(t.TraceEnded),\n\t\tTotalDurationMs: t.TotalDurationMS,\n\t\tSpanCount: t.SpanCount,\n\t\tErrorSpans: t.ErrorSpans,\n\t\tCycle: t.Cycle,\n\t}\n}\n\nfunc dbSpanToProto(s *db.Span) *omegav1.Span {\n\tspan := &omegav1.Span{\n\t\tSpanId: s.SpanID,\n\t\tTraceId: s.TraceID,\n\t\tParentSpanId: s.ParentSpanID,\n\t\tNodeId: s.NodeID,\n\t\tNodeName: s.NodeName,\n\t\tOperation: s.Operation,\n\t\tStartedAt: tsFromUnix(s.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(s.EndedAt),\n\t\tStatus: s.Status,\n\t\tCycle: s.Cycle,\n\t}\n\tif s.DurationMS != nil {\n\t\tspan.DurationMs = *s.DurationMS\n\t}\n\treturn span\n}\n\n// ── Metrics ───────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetMetrics(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsRequest],\n) (*connect.Response[omegav1.GetMetricsResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tcosts, _ := h.db.GetCosts()\n\tissues, _ := h.db.GetIssues(\"open\")\n\ttraces, _ := h.db.RecentTraces(5)\n\n\tresp := &omegav1.GetMetricsResponse{System: dbHealthToProto(health)}\n\tfor _, n := range nodes {\n\t\tresp.Nodes = append(resp.Nodes, dbNodeToProto(n))\n\t}\n\tfor _, c := range costs {\n\t\tresp.Costs = append(resp.Costs, dbCostToProto(c))\n\t}\n\tfor _, i := range issues {\n\t\tresp.OpenIssues = append(resp.OpenIssues, dbIssueToProto(i))\n\t}\n\tfor _, t := range traces {\n\t\tresp.RecentTraces = append(resp.RecentTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) GetMetricsHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsHistoryRequest],\n) (*connect.Response[omegav1.GetMetricsHistoryResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar histories []*omegav1.NodeLatencyHistory\n\tfor _, n := range nodes {\n\t\tif req.Msg.NodeId != \"\" && n.NodeID != req.Msg.NodeId {\n\t\t\tcontinue\n\t\t}\n\t\thistory, _ := h.db.LatencyHistory(n.NodeID, 100)\n\t\tnh := &omegav1.NodeLatencyHistory{NodeId: n.NodeID, Name: n.Name}\n\t\tfor _, p := range history {\n\t\t\tnh.History = append(nh.History, &omegav1.LatencyPoint{\n\t\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\t\tDurationMs: p.DurationMS,\n\t\t\t\tSuccess: p.Success,\n\t\t\t})\n\t\t}\n\t\thistories = append(histories, nh)\n\t}\n\treturn connect.NewResponse(&omegav1.GetMetricsHistoryResponse{NodeHistories: histories}), nil\n}\n\n// ── Issues ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListIssues(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListIssuesRequest],\n) (*connect.Response[omegav1.ListIssuesResponse], error) {\n\tissues, err := h.db.GetIssues(req.Msg.StateFilter)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoIssues []*omegav1.Issue\n\tfor _, i := range issues {\n\t\tprotoIssues = append(protoIssues, dbIssueToProto(i))\n\t}\n\treturn connect.NewResponse(&omegav1.ListIssuesResponse{Issues: protoIssues}), nil\n}\n\nfunc dbIssueToProto(i *db.Issue) *omegav1.Issue {\n\tissue := &omegav1.Issue{\n\t\tIssueId: i.IssueID,\n\t\tDetector: i.Detector,\n\t\tSeverity: i.Severity,\n\t\tDescription: i.Description,\n\t\tState: i.State,\n\t\tOpenedAt: tsFromUnix(i.OpenedAt),\n\t\tResolvedAt: tsFromUnixPtr(i.ResolvedAt),\n\t\tCycleOpened: i.CycleOpened,\n\t}\n\tif i.CycleResolved != nil {\n\t\tissue.CycleResolved = *i.CycleResolved\n\t}\n\treturn issue\n}\n\n// ── Activity ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListActivity(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListActivityRequest],\n) (*connect.Response[omegav1.ListActivityResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.RecentActivity(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.ActivityEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.ActivityEntry{\n\t\t\tLogId: e.LogID,\n\t\t\tActionType: e.ActionType,\n\t\t\tEntityType: e.EntityType,\n\t\t\tEntityId: e.EntityID,\n\t\t\tRecordedAt: tsFromUnix(e.RecordedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.ListActivityResponse{Entries: protoEntries}), nil\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListImprovements(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListImprovementsRequest],\n) (*connect.Response[omegav1.ListImprovementsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\timps, err := h.db.GetImprovements(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.ListImprovementsResponse{\n\t\tImprovements: dbImpsToProto(imps),\n\t}), nil\n}\n\nfunc dbImpsToProto(imps []*db.Improvement) []*omegav1.ImprovementRecord {\n\tvar result []*omegav1.ImprovementRecord\n\tfor _, imp := range imps {\n\t\tresult = append(result, &omegav1.ImprovementRecord{\n\t\t\tImproveId: imp.ImproveID,\n\t\t\tNodeId: imp.NodeID,\n\t\t\tNodeName: imp.NodeName,\n\t\t\tFromVersion: imp.FromVersion,\n\t\t\tToVersion: imp.ToVersion,\n\t\t\tTriggeredBy: imp.TriggeredBy,\n\t\t\tRecordedAt: tsFromUnix(imp.RecordedAt),\n\t\t\tCycle: imp.Cycle,\n\t\t\tBeforeMetrics: imp.BeforeMetrics,\n\t\t\tAfterMetrics: imp.AfterMetrics,\n\t\t})\n\t}\n\treturn result\n}\n\n// ── Memory ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetNodeMemory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeMemoryRequest],\n) (*connect.Response[omegav1.GetNodeMemoryResponse], error) {\n\t// Resolve node name for namespace\n\tvar namespace string\n\tif req.Msg.NodeId != \"\" {\n\t\tn, err := h.db.GetNode(req.Msg.NodeId)\n\t\tif err != nil {\n\t\t\treturn nil, connect.NewError(connect.CodeNotFound, err)\n\t\t}\n\t\tnamespace = n.Name\n\t}\n\n\tepCount, semCount, concepts, episodes, err := h.db.GetMemoryStats(namespace)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\tresp := &omegav1.GetNodeMemoryResponse{\n\t\tNamespace: namespace,\n\t\tEpisodicCount: epCount,\n\t\tSemanticCount: semCount,\n\t}\n\tfor _, c := range concepts {\n\t\tresp.TopSemantic = append(resp.TopSemantic, &omegav1.SemanticConcept{\n\t\t\tConcept: c.Concept,\n\t\t\tContent: c.Content,\n\t\t\tConfidence: c.Confidence,\n\t\t\tEvidenceCount: c.EvidenceCount,\n\t\t\tTags: c.Tags,\n\t\t})\n\t}\n\tfor _, e := range episodes {\n\t\tresp.RecentEpisodes = append(resp.RecentEpisodes, &omegav1.EpisodeEntry{\n\t\t\tEpisodeId: e.EpisodeID,\n\t\t\tEventType: e.EventType,\n\t\t\tTimestamp: tsFromUnix(e.Timestamp),\n\t\t\tCycle: e.Cycle,\n\t\t\tImportance: e.Importance,\n\t\t\tTags: e.Tags,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetConvergence(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetConvergenceRequest],\n) (*connect.Response[omegav1.GetConvergenceResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 100\n\t}\n\tpoints, err := h.db.GetConvergence(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoPoints []*omegav1.ConvergencePoint\n\tfor _, p := range points {\n\t\tprotoPoints = append(protoPoints, &omegav1.ConvergencePoint{\n\t\t\tCycle: p.Cycle,\n\t\t\tTimestamp: tsFromUnix(p.Timestamp),\n\t\t\tScore: p.Score,\n\t\t\tPipelineMs: p.PipelineMS,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetConvergenceResponse{Points: protoPoints}), nil\n}\n\n// ── Costs ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetCosts(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetCostsRequest],\n) (*connect.Response[omegav1.GetCostsResponse], error) {\n\tcosts, err := h.db.GetCosts()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoCosts []*omegav1.CostEntry\n\tfor _, c := range costs {\n\t\tprotoCosts = append(protoCosts, dbCostToProto(c))\n\t}\n\treturn connect.NewResponse(&omegav1.GetCostsResponse{Costs: protoCosts}), nil\n}\n\nfunc dbCostToProto(c *db.CostEntry) *omegav1.CostEntry {\n\treturn &omegav1.CostEntry{\n\t\tProvider: c.Provider,\n\t\tNodeId: c.NodeID,\n\t\tCalls: c.Calls,\n\t\tTotalMs: c.TotalMS,\n\t\tTotalCostUsd: c.TotalCostUSD,\n\t}\n}\n\n// ── Feedback ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) SubmitFeedback(\n\tctx context.Context,\n\treq *connect.Request[omegav1.SubmitFeedbackRequest],\n) (*connect.Response[omegav1.SubmitFeedbackResponse], error) {\n\t// Shell out to Python to store feedback in MemoryKernel\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-c\", fmt.Sprintf(`\nfrom omega.core.memory import MemoryKernel\nm = MemoryKernel(db_path='/tmp/omega_vectora_memory.db')\nm.store_episode('human_feedback', {'text': %q, 'target_node': %q}, tags=['feedback','human'], importance=0.9)\nprint('ok')\n`, req.Msg.Text, req.Msg.TargetNode))\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"feedback storage failed: %w\", err))\n\t}\n\treturn connect.NewResponse(&omegav1.SubmitFeedbackResponse{Ok: true}), nil\n}\n\n// ── Orchestrator control ──────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StartOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StartOrchestratorRequest],\n) (*connect.Response[omegav1.StartOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif h.orchRunning {\n\t\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\t\tStarted: false, Message: \"already running\",\n\t\t}), nil\n\t}\n\theartbeat := req.Msg.HeartbeatSecs\n\tif heartbeat <= 0 {\n\t\theartbeat = 120\n\t}\n\tcmd := exec.Command(\"python3\", \"-m\", \"omega.examples.vectora_main\",\n\t\t\"--heartbeat\", fmt.Sprintf(\"%d\", heartbeat))\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"start orchestrator: %w\", err))\n\t}\n\th.orchCmd = cmd\n\th.orchRunning = true\n\t// Watch for exit\n\tgo func() {\n\t\tcmd.Wait() //nolint:errcheck\n\t\th.mu.Lock()\n\t\th.orchRunning = false\n\t\th.orchCmd = nil\n\t\th.mu.Unlock()\n\t}()\n\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\tStarted: true, Message: \"started\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) StopOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StopOrchestratorRequest],\n) (*connect.Response[omegav1.StopOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif !h.orchRunning || h.orchCmd == nil {\n\t\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\t\tStopped: false, Message: \"not running\",\n\t\t}), nil\n\t}\n\tif err := h.orchCmd.Process.Kill(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\th.orchRunning = false\n\th.orchCmd = nil\n\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\tStopped: true, Message: \"stopped\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) TriggerHeartbeat(\n\tctx context.Context,\n\treq *connect.Request[omegav1.TriggerHeartbeatRequest],\n) (*connect.Response[omegav1.TriggerHeartbeatResponse], error) {\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-m\", \"omega.examples.vectora_main\", \"--once\")\n\tgo cmd.Run() //nolint:errcheck\n\treturn connect.NewResponse(&omegav1.TriggerHeartbeatResponse{\n\t\tTriggered: true, Message: \"triggered single heartbeat\",\n\t}), nil\n}\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StreamEvents(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StreamEventsRequest],\n\tstream *connect.ServerStream[omegav1.EventResponse],\n) error {\n\tinterval := time.Duration(req.Msg.PollIntervalMs) * time.Millisecond\n\tif interval < 500*time.Millisecond {\n\t\tinterval = 2 * time.Second\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase <-ticker.C:\n\t\t\thealth, err := h.db.SystemHealth()\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := stream.Send(&omegav1.EventResponse{\n\t\t\t\tType: \"health_update\",\n\t\t\t\tPayload: &omegav1.EventResponse_HealthUpdate{\n\t\t\t\t\tHealthUpdate: dbHealthToProto(health),\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ── Brain config RPCs ─────────────────────────────────────────────────────────\n\n// knownProviders lists all supported LLM providers and their available models.\nvar knownProviders = []*omegav1.ProviderInfo{\n\t{Id: \"anthropic\", DisplayName: \"Anthropic\", Models: []string{\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"openai\", DisplayName: \"OpenAI\", Models: []string{\"gpt-4o\", \"gpt-4o-mini\", \"o1\", \"o3-mini\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"google\", DisplayName: \"Google\", Models: []string{\"gemini-2.0-flash\", \"gemini-2.0-pro\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"deepseek\", DisplayName: \"DeepSeek\", Models: []string{\"deepseek-chat\", \"deepseek-reasoner\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"ollama\", DisplayName: \"Ollama (local)\", Models: []string{\"llama3.3\", \"qwen2.5-coder\", \"mistral\"}, RequiresApiKey: false, Available: true},\n}\n\nfunc (h *OrchestratorHandler) ListAvailableProviders(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListAvailableProvidersRequest],\n) (*connect.Response[omegav1.ListAvailableProvidersResponse], error) {\n\treturn connect.NewResponse(&omegav1.ListAvailableProvidersResponse{\n\t\tProviders: knownProviders,\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNodeConfig(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeConfigRequest],\n) (*connect.Response[omegav1.GetNodeConfigResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\tbrain, err := h.db.GetBrainConfig(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetNodeConfigResponse{\n\t\tConfig: dbNodeConfigToProto(n, brain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) UpdateNodeBrain(\n\tctx context.Context,\n\treq *connect.Request[omegav1.UpdateNodeBrainRequest],\n) (*connect.Response[omegav1.UpdateNodeBrainResponse], error) {\n\tbrain := &db.BrainConfig{\n\t\tNodeID: req.Msg.NodeId,\n\t\tProvider: req.Msg.Brain.Provider,\n\t\tModel: req.Msg.Brain.Model,\n\t\tTemperature: req.Msg.Brain.Temperature,\n\t\tMaxTokens: int64(req.Msg.Brain.MaxTokens),\n\t\tSystemPrompt: req.Msg.Brain.SystemPrompt,\n\t\tExtraConfig: req.Msg.Brain.ExtraConfig,\n\t}\n\tif err := h.db.SetBrainConfig(brain); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tn, _ := h.db.GetNode(req.Msg.NodeId)\n\tupdatedBrain, _ := h.db.GetBrainConfig(req.Msg.NodeId)\n\treturn connect.NewResponse(&omegav1.UpdateNodeBrainResponse{\n\t\tConfig: dbNodeConfigToProto(n, updatedBrain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetBrainExecutionHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetBrainExecutionHistoryRequest],\n) (*connect.Response[omegav1.GetBrainExecutionHistoryResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.GetBrainHistory(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.BrainExecutionEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.BrainExecutionEntry{\n\t\t\tExecId: e.ExecID,\n\t\t\tNodeId: e.NodeID,\n\t\t\tProvider: e.Provider,\n\t\t\tModel: e.Model,\n\t\t\tPromptTokens: e.PromptTokens,\n\t\t\tCompletionTokens: e.CompletionTokens,\n\t\t\tLatencyMs: e.LatencyMS,\n\t\t\tSuccess: e.Success,\n\t\t\tErrorText: e.ErrorText,\n\t\t\tExecutedAt: tsFromUnix(e.ExecutedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetBrainExecutionHistoryResponse{Entries: protoEntries}), nil\n}\n\nfunc dbNodeConfigToProto(n *db.Node, brain *db.BrainConfig) *omegav1.NodeConfig {\n\tconfig := &omegav1.NodeConfig{\n\t\tNodeId: n.NodeID,\n\t\tNodeType: n.Name,\n\t\tVersion: n.Version,\n\t\tBrain: &omegav1.BrainConfig{\n\t\t\tProvider: brain.Provider,\n\t\t\tModel: brain.Model,\n\t\t\tTemperature: brain.Temperature,\n\t\t\tMaxTokens: int32(brain.MaxTokens),\n\t\t\tSystemPrompt: brain.SystemPrompt,\n\t\t\tExtraConfig: brain.ExtraConfig,\n\t\t},\n\t\tUpdatedAt: tsFromUnix(brain.UpdatedAt),\n\t}\n\treturn config\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/handler/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/handler/\ngit commit -m \"feat(handler): implement OrchestratorService + brain config Connect-RPC handlers\"\n```\n\n---\n\n## Task 5: Go API Server Entry Point\n\n**Files:**\n- Create: `cmd/omega-api/main.go`\n\n- [ ] **Step 1: Write `cmd/omega-api/main.go`**\n\n```go\n// Command omega-api serves the Omega dashboard Connect-RPC API.\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"connectrpc.com/connect\"\n\tconnectcors \"connectrpc.com/connect/cors\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n\t\"github.com/benebsworth/omega/internal/handler\"\n)\n\nfunc main() {\n\tdatabase, err := db.New(db.StateDBPath, db.MemoryDBPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"open DB: %v\", err)\n\t}\n\tdefer database.Close()\n\n\th := handler.New(database)\n\tmux := http.NewServeMux()\n\n\t// Buf's connectrpc/go plugin generates NewOrchestratorServiceHandler in the same package.\n\tpath, svcHandler := omegav1.NewOrchestratorServiceHandler(h,\n\t\tconnect.WithCompressMinBytes(1024),\n\t)\n\tmux.Handle(path, svcHandler)\n\n\t// CORS — use connectrpc.com/connect/cors helpers to include streaming headers.\n\tcorsHandler := withCORS(mux)\n\n\taddr := \":8080\"\n\tlog.Printf(\"Omega API listening on %s (Connect-RPC)\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, h2c.NewHandler(corsHandler, &http2.Server{})))\n}\n\nfunc withCORS(h http.Handler) http.Handler {\n\tallowedHeaders := append(connectcors.AllowedHeaders(), \"X-User-Agent\")\n\texposedHeaders := connectcors.ExposedHeaders()\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", strings.Join(allowedHeaders, \", \"))\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", strings.Join(exposedHeaders, \", \"))\n\t\tif r.Method == http.MethodOptions {\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\treturn\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t})\n}\n```\n\n- [ ] **Step 2: Build and verify**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api/...\n```\n\nExpected: produces `./omega-api` (or similar) without errors.\n\n- [ ] **Step 3: Smoke test — start server and verify it responds**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\ncurl -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" \\\n -d '{}' | python3 -m json.tool\nkill %1\n```\n\nExpected: JSON response with health data (or empty `{}` if DB doesn't exist yet).\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add cmd/\ngit commit -m \"feat(cmd): add omega-api Connect-RPC server entry point\"\n```\n\n---\n\n## Task 6: Frontend Scaffold + Connect-ES Setup\n\n**Files:**\n- Create: `dashboard/` — Vite React TS project\n\n- [ ] **Step 1: Scaffold Vite project at repo root**\n\n```bash\ncd /Users/benebsworth/projects/omega\nnpm create vite@latest dashboard -- --template react-ts\n```\n\n- [ ] **Step 2: Install dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm install\nnpm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf\nnpm install react-router-dom recharts lucide-react\nnpm install -D tailwindcss postcss autoprefixer @types/node\nnpx tailwindcss init -p\n```\n\n- [ ] **Step 3: Configure Tailwind — `tailwind.config.js`**\n\n```js\n/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n theme: {\n extend: {\n colors: {\n surface: {\n 900: \"#0f1117\",\n 800: \"#161b27\",\n 700: \"#1e2533\",\n 600: \"#252d3d\",\n },\n },\n },\n },\n plugins: [],\n}\n```\n\n- [ ] **Step 4: Replace `src/index.css`**\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n- [ ] **Step 5: Configure Vite proxy — `vite.config.ts`**\n\n```ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n port: 5173,\n proxy: {\n // Proxy all Connect-RPC calls to the Go server\n \"/omega.v1\": {\n target: \"http://localhost:8080\",\n changeOrigin: true,\n },\n },\n },\n});\n```\n\n- [ ] **Step 6: Write `src/client.ts` — Connect transport + service client**\n\n```typescript\nimport { createConnectTransport } from \"@connectrpc/connect-web\";\nimport { createClient } from \"@connectrpc/connect\";\nimport { OrchestratorService } from \"./gen/omega/v1/omega_service_pb\";\n\n// In dev, Vite proxies /omega.v1/* → http://localhost:8080\n// In production, VITE_API_URL should point to the Go server\nconst baseUrl = import.meta.env.VITE_API_URL ?? \"\";\n\nexport const transport = createConnectTransport({\n baseUrl,\n // Use the Connect protocol (works over HTTP/1.1 without gRPC-web)\n});\n\nexport const client = createClient(OrchestratorService, transport);\n```\n\n- [ ] **Step 7: Commit scaffold**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/\ngit commit -m \"feat(dashboard): scaffold Vite React TS with Connect-ES + Tailwind\"\n```\n\n---\n\n## Task 7: Layout Components\n\n**Files:**\n- Create: `dashboard/src/components/layout/Sidebar.tsx`\n- Create: `dashboard/src/components/layout/Header.tsx`\n- Create: `dashboard/src/App.tsx`\n\n- [ ] **Step 1: Write `Sidebar.tsx`**\n\n```tsx\nimport { NavLink } from \"react-router-dom\";\nimport {\n LayoutDashboard, Server, GitBranch, BarChart2,\n AlertTriangle, Brain, TrendingUp,\n} from \"lucide-react\";\n\nconst NAV = [\n { to: \"/\", icon: LayoutDashboard, label: \"Dashboard\" },\n { to: \"/nodes\", icon: Server, label: \"Nodes\" },\n { to: \"/traces\", icon: GitBranch, label: \"Traces\" },\n { to: \"/metrics\", icon: BarChart2, label: \"Metrics\" },\n { to: \"/issues\", icon: AlertTriangle, label: \"Issues\" },\n { to: \"/memory\", icon: Brain, label: \"Memory\" },\n { to: \"/convergence\", icon: TrendingUp, label: \"Convergence\" },\n];\n\nexport default function Sidebar() {\n return (\n \n );\n}\n```\n\n- [ ] **Step 2: Write `Header.tsx`**\n\n```tsx\nimport { useState } from \"react\";\nimport { client } from \"../../client\";\nimport { Play, Square, Zap } from \"lucide-react\";\n\ninterface HeaderProps {\n systemStatus?: string;\n connected?: boolean;\n}\n\nexport default function Header({ systemStatus, connected }: HeaderProps) {\n const [loading, setLoading] = useState(null);\n\n const statusColor = {\n healthy: \"bg-green-500\",\n degraded: \"bg-yellow-500\",\n critical: \"bg-red-500\",\n no_nodes: \"bg-gray-500\",\n }[systemStatus ?? \"no_nodes\"] ?? \"bg-gray-500\";\n\n async function action(name: \"start\" | \"stop\" | \"trigger\") {\n setLoading(name);\n try {\n if (name === \"start\") await client.startOrchestrator({ heartbeatSecs: 120 });\n else if (name === \"stop\") await client.stopOrchestrator({});\n else await client.triggerHeartbeat({});\n } finally {\n setLoading(null);\n }\n }\n\n return (\n
\n
\n \n \n {systemStatus ?? \"unknown\"}\n \n {connected !== undefined && (\n \n {connected ? \"streaming\" : \"offline\"}\n \n )}\n
\n
\n {([\"start\", \"trigger\", \"stop\"] as const).map((name) => (\n action(name)}\n disabled={!!loading}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors disabled:opacity-50 ${\n name === \"start\" ? \"bg-green-700 hover:bg-green-600\" :\n name === \"trigger\" ? \"bg-indigo-700 hover:bg-indigo-600\" :\n \"bg-red-800 hover:bg-red-700\"\n }`}\n >\n {name === \"start\" ? : name === \"stop\" ? : }\n {name.charAt(0).toUpperCase() + name.slice(1)}\n \n ))}\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Write `App.tsx`**\n\n```tsx\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Sidebar from \"./components/layout/Sidebar\";\nimport Header from \"./components/layout/Header\";\nimport { client } from \"./client\";\nimport type { SystemHealth } from \"./gen/omega/v1/types_pb\";\nimport Dashboard from \"./pages/Dashboard\";\nimport Nodes from \"./pages/Nodes\";\nimport Traces from \"./pages/Traces\";\nimport Metrics from \"./pages/Metrics\";\nimport Issues from \"./pages/Issues\";\nimport Memory from \"./pages/Memory\";\nimport Convergence from \"./pages/Convergence\";\n\nexport default function App() {\n const [health, setHealth] = useState(null);\n const [streamConnected, setStreamConnected] = useState(false);\n const abortRef = useRef(null);\n\n useEffect(() => {\n startStream();\n return () => abortRef.current?.abort();\n }, []);\n\n async function startStream() {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n setStreamConnected(true);\n for await (const event of client.streamEvents(\n { pollIntervalMs: 2000 },\n { signal: controller.signal }\n )) {\n if (event.payload.case === \"healthUpdate\") {\n setHealth(event.payload.value);\n }\n }\n } catch {\n setStreamConnected(false);\n // Reconnect after 3s\n setTimeout(startStream, 3000);\n }\n }\n\n return (\n \n
\n \n
\n \n
\n \n } />\n } />\n } />\n } />\n } />\n } />\n } />\n } />\n \n
\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 4: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/src/\ngit commit -m \"feat(dashboard): add layout + App router with server-streaming health\"\n```\n\n---\n\n## Task 8: Dashboard Page\n\n**Files:**\n- Create: `dashboard/src/pages/Dashboard.tsx`\n- Create: `dashboard/src/components/NodeCard.tsx`\n\n- [ ] **Step 1: Write `NodeCard.tsx`**\n\n```tsx\nimport { LineChart, Line, ResponsiveContainer } from \"recharts\";\nimport type { Node } from \"../gen/omega/v1/types_pb\";\n\nfunction healthColor(h: number) {\n if (h >= 0.8) return \"border-green-600 bg-green-900/20\";\n if (h >= 0.6) return \"border-yellow-600 bg-yellow-900/20\";\n return \"border-red-600 bg-red-900/20\";\n}\n\nfunction healthText(h: number) {\n if (h >= 0.8) return \"text-green-400\";\n if (h >= 0.6) return \"text-yellow-400\";\n return \"text-red-400\";\n}\n\ninterface NodeCardProps { node: Node }\n\nexport default function NodeCard({ node }: NodeCardProps) {\n const sparkData = node.latencyHistory.slice(-20).map((p) => ({ v: p.durationMs }));\n const lastExecAgo = node.lastExecution?.startedAt\n ? Math.round((Date.now() / 1000 - Number(node.lastExecution.startedAt.seconds)) / 60)\n : null;\n\n return (\n
\n
\n
\n

{node.name}

\n

v{node.version}

\n
\n \n {(node.health * 100).toFixed(0)}%\n \n
\n {sparkData.length > 1 && (\n
\n \n \n = 0.8 ? \"#4ade80\" : node.health >= 0.6 ? \"#facc15\" : \"#f87171\"}\n dot={false} strokeWidth={1.5} />\n \n \n
\n )}\n
\n err: {(node.errorRate * 100).toFixed(1)}%\n {lastExecAgo !== null ? `${lastExecAgo}m ago` : \"never\"}\n
\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Dashboard.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport NodeCard from \"../components/NodeCard\";\nimport type { SystemHealth, Node, ActivityEntry } from \"../gen/omega/v1/types_pb\";\n\ninterface DashboardProps { health: SystemHealth | null }\n\nexport default function Dashboard({ health }: DashboardProps) {\n const [nodes, setNodes] = useState([]);\n const [activity, setActivity] = useState([]);\n\n useEffect(() => {\n client.listNodes({}).then((r) => setNodes(r.nodes)).catch(console.error);\n client.listActivity({ limit: 20 }).then((r) => setActivity(r.entries)).catch(console.error);\n }, []);\n\n const statusColor: Record = {\n healthy: \"text-green-400\",\n degraded: \"text-yellow-400\",\n critical: \"text-red-400\",\n };\n\n return (\n
\n {/* System stats */}\n
\n {[\n { label: \"Status\", value: health?.status ?? \"—\", cls: statusColor[health?.status ?? \"\"] ?? \"text-white\" },\n { label: \"Health Score\", value: health ? `${(health.compositeScore * 100).toFixed(1)}%` : \"—\", cls: \"text-white\" },\n { label: \"Total Cycles\", value: String(health?.totalCycles ?? \"—\"), cls: \"text-white\" },\n { label: \"Open Issues\", value: String(health?.openIssues ?? \"—\"), cls: (health?.openIssues ?? 0) > 0 ? \"text-red-400\" : \"text-white\" },\n ].map(({ label, value, cls }) => (\n
\n

{label}

\n

{value}

\n
\n ))}\n
\n\n {/* Node grid */}\n
\n

\n Nodes ({nodes.length})\n

\n
\n {nodes.map((n) => )}\n
\n {nodes.length === 0 && (\n

\n No nodes registered — run the orchestrator first.\n

\n )}\n
\n\n {/* Activity feed */}\n
\n

\n Recent Activity\n

\n
\n {activity.map((evt) => (\n
\n \n {evt.recordedAt\n ? new Date(Number(evt.recordedAt.seconds) * 1000).toLocaleTimeString()\n : \"—\"}\n \n {evt.actionType}\n {evt.entityId}\n
\n ))}\n {activity.length === 0 && (\n

No activity yet.

\n )}\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/Dashboard.tsx dashboard/src/components/NodeCard.tsx\ngit commit -m \"feat(dashboard): add Dashboard page using Connect-ES typed client\"\n```\n\n---\n\n## Task 9: Traces Page with Waterfall\n\n**Files:**\n- Create: `dashboard/src/components/TraceWaterfall.tsx`\n- Create: `dashboard/src/pages/Traces.tsx`\n\n- [ ] **Step 1: Write `TraceWaterfall.tsx`**\n\n```tsx\nimport type { Span } from \"../gen/omega/v1/types_pb\";\n\ninterface TraceWaterfallProps { spans: Span[] }\n\nexport default function TraceWaterfall({ spans }: TraceWaterfallProps) {\n if (!spans.length) return

No spans.

;\n\n const sorted = [...spans].sort(\n (a, b) => Number(a.startedAt?.seconds ?? 0) - Number(b.startedAt?.seconds ?? 0)\n );\n const minTs = Number(sorted[0]?.startedAt?.seconds ?? 0);\n const maxTs = sorted.reduce(\n (m, s) => Math.max(m, Number(s.endedAt?.seconds ?? s.startedAt?.seconds ?? 0)), 0\n );\n const totalMs = Math.max(1, (maxTs - minTs) * 1000);\n\n const depthMap = new Map();\n for (const span of sorted) {\n depthMap.set(span.spanId, span.parentSpanId ? (depthMap.get(span.parentSpanId) ?? 0) + 1 : 0);\n }\n\n return (\n
\n {sorted.map((span) => {\n const depth = depthMap.get(span.spanId) ?? 0;\n const offsetMs = (Number(span.startedAt?.seconds ?? 0) - minTs) * 1000;\n const durMs = span.durationMs;\n const leftPct = (offsetMs / totalMs) * 100;\n const widthPct = Math.max(0.5, (durMs / totalMs) * 100);\n const isErr = span.status === \"error\";\n\n return (\n
\n
\n {isErr ? \"✗\" : \"✓\"}{\" \"}\n {span.nodeName || span.operation}\n
\n
\n \n
\n
{durMs.toFixed(0)}ms
\n
\n );\n })}\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Traces.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport TraceWaterfall from \"../components/TraceWaterfall\";\nimport type { TraceSummary, Span } from \"../gen/omega/v1/types_pb\";\n\nexport default function Traces() {\n const [traces, setTraces] = useState([]);\n const [selected, setSelected] = useState(null);\n const [spans, setSpans] = useState([]);\n\n useEffect(() => {\n client.listTraces({ limit: 30 }).then((r) => setTraces(r.traces)).catch(console.error);\n }, []);\n\n function selectTrace(id: string) {\n setSelected(id);\n client.getTrace({ traceId: id }).then((r) => setSpans(r.spans)).catch(console.error);\n }\n\n return (\n
\n
\n
\n Recent Traces\n
\n {traces.map((t) => (\n \n ))}\n {traces.length === 0 &&

No traces yet

}\n
\n
\n {selected\n ? <>

trace: {selected}

\n :

Select a trace to view the waterfall

\n }\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/components/TraceWaterfall.tsx dashboard/src/pages/Traces.tsx\ngit commit -m \"feat(dashboard): add trace waterfall viewer with typed Connect-ES\"\n```\n\n---\n\n## Task 10: Remaining Pages + Brain Config UI\n\n**Files:**\n- Create: `dashboard/src/pages/Nodes.tsx`\n- Create: `dashboard/src/components/BrainConfigPanel.tsx`\n- Create: `dashboard/src/pages/Metrics.tsx`\n- Create: `dashboard/src/pages/Issues.tsx`\n- Create: `dashboard/src/pages/Memory.tsx`\n- Create: `dashboard/src/pages/Convergence.tsx`\n\n- [ ] **Step 1: Write `BrainConfigPanel.tsx` — model switcher component**\n\n```tsx\nimport { useState, useEffect } from \"react\";\nimport { client } from \"../client\";\nimport type { NodeConfig, ProviderInfo } from \"../gen/omega/v1/types_pb\";\n\ninterface Props { nodeId: string }\n\nexport default function BrainConfigPanel({ nodeId }: Props) {\n const [config, setConfig] = useState(null);\n const [providers, setProviders] = useState([]);\n const [saving, setSaving] = useState(false);\n const [draft, setDraft] = useState({ provider: \"\", model: \"\", temperature: 0.7, maxTokens: 4096 });\n\n useEffect(() => {\n if (!nodeId) return;\n client.getNodeConfig({ nodeId }).then((r) => {\n setConfig(r.config ?? null);\n if (r.config?.brain) {\n const b = r.config.brain;\n setDraft({ provider: b.provider, model: b.model, temperature: b.temperature, maxTokens: b.maxTokens });\n }\n });\n client.listAvailableProviders({}).then((r) => setProviders(r.providers));\n }, [nodeId]);\n\n const currentProvider = providers.find((p) => p.id === draft.provider);\n\n async function save() {\n setSaving(true);\n try {\n const r = await client.updateNodeBrain({\n nodeId,\n brain: { provider: draft.provider, model: draft.model, temperature: draft.temperature, maxTokens: draft.maxTokens, systemPrompt: config?.brain?.systemPrompt ?? \"\", extraConfig: {} },\n });\n setConfig(r.config ?? null);\n } finally {\n setSaving(false);\n }\n }\n\n if (!config) return
Loading brain config…
;\n\n return (\n
\n

\n 🧠 LLM Brain Config\n

\n
\n
\n \n setDraft({ ...draft, provider: e.target.value, model: \"\" })}\n >\n {providers.map((p) => )}\n \n
\n
\n \n setDraft({ ...draft, model: e.target.value })}\n >\n {(currentProvider?.models ?? []).map((m) => )}\n \n
\n
\n \n setDraft({ ...draft, temperature: parseFloat(e.target.value) })}\n />\n
\n
\n \n setDraft({ ...draft, maxTokens: parseInt(e.target.value, 10) })}\n />\n
\n
\n \n {saving ? \"Saving…\" : \"Apply Brain Config\"}\n \n
\n );\n}\n```\n\n- [ ] **Step 2: Write stubs for remaining pages (iterate to full implementations)**\n\nEach page follows the same pattern: call `client.methodName({})`, display results. Write full implementations using the generated types.\n\n`Nodes.tsx` — calls `client.listNodes`, `client.getNode`, `client.getMetricsHistory`; includes `` in the node detail panel\n`Metrics.tsx` — calls `client.getMetricsHistory`, renders per-node `LineChart` with recharts\n`Issues.tsx` — calls `client.listIssues({ stateFilter: \"open\" })`, renders sortable table\n`Memory.tsx` — calls `client.getNodeMemory({ nodeId: \"\" })` for global view, semantic concept list\n`Convergence.tsx` — calls `client.getConvergence`, `client.listImprovements`, renders score chart\n\nThese pages follow the same structure as shown in Tasks 8 and 9. Write them using the generated proto types from `./gen/omega/v1/types_pb`.\n\n**`Nodes.tsx` detail panel pattern:**\n```tsx\n// When a node is selected, show node stats + brain config side by side\n{selectedNode && (\n
\n
\n {/* node stats, latency chart, execution history */}\n
\n \n
\n)}\n```\n\n- [ ] **Step 2: Verify build**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run build 2>&1 | tail -10\n```\n\nExpected: `built in Xs` with no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/\ngit commit -m \"feat(dashboard): complete all 7 pages\"\n```\n\n---\n\n## Task 11: End-to-End Verification\n\n- [ ] **Step 1: Run one Python heartbeat to populate SQLite DBs**\n\n```bash\ncd /Users/benebsworth/projects/omega\npython -m omega.examples.vectora_main --once\n```\n\nExpected: heartbeat runs, data written to `/tmp/omega_vectora_state.db`.\n\n- [ ] **Step 2: Start Go API server**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\n```\n\n- [ ] **Step 3: Verify key endpoints**\n\n```bash\n# Health\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Nodes\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool | head -30\n\n# Traces\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListTraces \\\n -H \"Content-Type: application/json\" -d '{\"limit\": 5}' | python3 -m json.tool\n\n# Brain config — list providers\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListAvailableProviders \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Brain config — get node config (replace NODE_ID with a real node_id from ListNodes)\nNODE_ID=$(curl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['nodes'][0]['nodeId'])\" 2>/dev/null)\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetNodeConfig \\\n -H \"Content-Type: application/json\" -d \"{\\\"nodeId\\\": \\\"$NODE_ID\\\"}\" | python3 -m json.tool\n```\n\nExpected: nodes with health scores, at least one trace, providers list with 5 entries, node config defaulting to `claude-sonnet-4-6`.\n\n- [ ] **Step 4: Start frontend**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev &\nsleep 2\necho \"Dashboard at http://localhost:5173\"\n```\n\n- [ ] **Step 5: Verify via Playwright MCP**\n\nUse `mcp__plugin_playwright_playwright__browser_navigate` to visit `http://localhost:5173` and take a screenshot to confirm the dashboard loads.\n\n- [ ] **Step 6: Final commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add -A\ngit commit -m \"feat: complete Omega Go+Connect-RPC API and React dashboard\"\n```\n\n---\n\n## Quick-start (reference)\n\n```bash\n# Terminal 1: Go API\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api && ./omega-api\n\n# Terminal 2: React dashboard\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev\n\n# Terminal 3: Python orchestrator\npython -m omega.examples.vectora_main --heartbeat 60\n\n# Open dashboard\nopen http://localhost:5173\n```\n"}, "ac2e7b5d59a3f0afc11851a1ad6ea24c02041841": {"t": 1786067777.7165189, "n": 100004, "h": "0bdffc42d150ad01", "c": "# Omega Web Dashboard — Go + Connect-RPC + Protobuf Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Build a Go Connect-RPC API server + React/TypeScript dashboard with Connect-ES that reads from Omega's SQLite state store and renders live visibility into the self-improving agent system.\n\n**Architecture:** Proto definitions are the contract. Go server (`cmd/omega-api`) reads from the shared SQLite DBs that the Python node layer writes to. React dashboard uses `@connectrpc/connect-web` to call the Go server directly — no REST, no WebSockets. Real-time updates use server-streaming RPC (`StreamEvents`).\n\n**Tech Stack:** Go 1.24+, `connectrpc.com/connect`, `modernc.org/sqlite`, `google.golang.org/protobuf`, Buf CLI; Vite 5, React 18, TypeScript, `@connectrpc/connect-web` v2, `@bufbuild/protobuf`, Tailwind CSS, shadcn/ui, recharts\n\n---\n\n## Key Data Facts (from existing Python codebase)\n\n- **State store DB**: `/tmp/omega_victoria_state.db`\n - Tables: `nodes`, `node_executions`, `traces`, `cost_events`, `issues`, `activity_log`, `improvement_log`, `config_revisions`\n- **Memory DB**: `/tmp/omega_victoria_memory.db`\n - Tables: `episodes`, `semantic_memories`, `memory_ratings`\n- **Convergence data**: `episodes WHERE event_type='cycle_summary'` contains `score`, `pipeline_ms` per cycle\n- **Node namespacing**: memory is namespaced by node name (e.g., `\"DataIngestionNode\"`)\n- **Toolchain available**: Go 1.26, Buf 1.47, Node 22, npm 10\n\n---\n\n## File Map\n\n### Proto definitions\n| File | Responsibility |\n|------|---------------|\n| `buf.yaml` | Buf workspace config |\n| `buf.gen.yaml` | Code generation config (Go + TypeScript) |\n| `proto/omega/v1/omega_service.proto` | OrchestratorService — all dashboard RPCs |\n| `proto/omega/v1/node_service.proto` | NodeService — inter-node communication contract |\n| `proto/omega/v1/types.proto` | Shared message types (Node, Span, Issue, etc.) |\n\n### Go backend\n| File | Responsibility |\n|------|---------------|\n| `go.mod` | Go module definition |\n| `cmd/omega-api/main.go` | Server entry point, Connect handler registration |\n| `internal/db/db.go` | SQLite read layer — all queries |\n| `internal/handler/orchestrator.go` | OrchestratorService implementation |\n\n### Generated (do not hand-edit)\n| Path | Generated by |\n|------|-------------|\n| `gen/go/omega/v1/` | buf generate → Go protobuf + connect stubs |\n| `dashboard/src/gen/` | buf generate → TypeScript protobuf + connect clients |\n\n### Frontend\n| File | Responsibility |\n|------|---------------|\n| `dashboard/src/client.ts` | Connect transport + service client factory |\n| `dashboard/src/components/layout/Sidebar.tsx` | Nav sidebar |\n| `dashboard/src/components/layout/Header.tsx` | Status bar + orchestrator controls |\n| `dashboard/src/components/NodeCard.tsx` | Node health card with sparkline |\n| `dashboard/src/components/TraceWaterfall.tsx` | SVG waterfall for span trees |\n| `dashboard/src/components/BrainConfigPanel.tsx` | LLM model switcher panel (provider + model selectors) |\n| `dashboard/src/pages/Dashboard.tsx` | Overview page |\n| `dashboard/src/pages/Nodes.tsx` | Node table + detail + BrainConfigPanel |\n| `dashboard/src/pages/Traces.tsx` | Trace list + waterfall |\n| `dashboard/src/pages/Metrics.tsx` | Time-series charts |\n| `dashboard/src/pages/Issues.tsx` | Issues table |\n| `dashboard/src/pages/Memory.tsx` | Memory kernel visualization |\n| `dashboard/src/pages/Convergence.tsx` | Convergence + improvement log |\n| `dashboard/src/App.tsx` | Router + layout wrapper |\n\n---\n\n## Task 1: Proto Definitions\n\n**Files:**\n- Create: `buf.yaml`\n- Create: `buf.gen.yaml`\n- Create: `proto/omega/v1/types.proto`\n- Create: `proto/omega/v1/omega_service.proto`\n- Create: `proto/omega/v1/node_service.proto`\n\n- [ ] **Step 1: Write `buf.yaml`**\n\n```yaml\nversion: v2\nmodules:\n - path: proto\ndeps:\n - buf.build/googleapis/googleapis\nlint:\n use:\n - STANDARD\nbreaking:\n use:\n - FILE\n```\n\n- [ ] **Step 2: Write `buf.gen.yaml`**\n\n```yaml\nversion: v2\nmanaged:\n enabled: true\nplugins:\n - remote: buf.build/protocolbuffers/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/connectrpc/go\n out: gen/go\n opt:\n - paths=source_relative\n - remote: buf.build/bufbuild/es\n out: dashboard/src/gen\n opt:\n - target=ts\n - remote: buf.build/connectrpc/es\n out: dashboard/src/gen\n opt:\n - target=ts\n```\n\n- [ ] **Step 3: Write `proto/omega/v1/types.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage Node {\n string node_id = 1;\n string name = 2;\n string version = 3;\n repeated string capabilities = 4;\n double health = 5;\n string status = 6;\n google.protobuf.Timestamp registered_at = 7;\n google.protobuf.Timestamp last_updated = 8;\n int64 executions_total = 9;\n double error_rate = 10;\n double avg_latency_ms = 11;\n double p95_latency_ms = 12;\n int64 improvement_count = 13;\n ExecutionRecord last_execution = 14;\n}\n\nmessage ExecutionRecord {\n string exec_id = 1;\n string node_id = 2;\n string node_name = 3;\n string trace_id = 4;\n string span_id = 5;\n string action = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n bool success = 10;\n string error_text = 11;\n map metrics = 12;\n int64 cycle = 13;\n}\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage TraceSummary {\n string trace_id = 1;\n google.protobuf.Timestamp trace_started = 2;\n google.protobuf.Timestamp trace_ended = 3;\n double total_duration_ms = 4;\n int64 span_count = 5;\n int64 error_spans = 6;\n int64 cycle = 7;\n}\n\nmessage Span {\n string span_id = 1;\n string trace_id = 2;\n string parent_span_id = 3;\n string node_id = 4;\n string node_name = 5;\n string operation = 6;\n google.protobuf.Timestamp started_at = 7;\n google.protobuf.Timestamp ended_at = 8;\n double duration_ms = 9;\n string status = 10;\n int64 cycle = 11;\n}\n\n// ── Issues ─────────────────────────────────────────────────────────────────\n\nmessage Issue {\n string issue_id = 1;\n string detector = 2;\n string severity = 3;\n string description = 4;\n string state = 5;\n google.protobuf.Timestamp opened_at = 6;\n google.protobuf.Timestamp resolved_at = 7;\n int64 cycle_opened = 8;\n int64 cycle_resolved = 9;\n}\n\n// ── Activity ───────────────────────────────────────────────────────────────\n\nmessage ActivityEntry {\n string log_id = 1;\n string action_type = 2;\n string entity_type = 3;\n string entity_id = 4;\n google.protobuf.Timestamp recorded_at = 5;\n int64 cycle = 6;\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────\n\nmessage ImprovementRecord {\n string improve_id = 1;\n string node_id = 2;\n string node_name = 3;\n string from_version = 4;\n string to_version = 5;\n string triggered_by = 6;\n google.protobuf.Timestamp recorded_at = 7;\n int64 cycle = 8;\n map before_metrics = 9;\n map after_metrics = 10;\n}\n\n// ── System health ──────────────────────────────────────────────────────────\n\nmessage SystemHealth {\n string status = 1;\n double composite_score = 2;\n double avg_node_health = 3;\n int64 node_count = 4;\n int64 open_issues = 5;\n int64 error_issues = 6;\n double uptime_seconds = 7;\n int64 total_cycles = 8;\n}\n\n// ── Memory ─────────────────────────────────────────────────────────────────\n\nmessage SemanticConcept {\n string concept = 1;\n string content = 2;\n double confidence = 3;\n int64 evidence_count = 4;\n repeated string tags = 5;\n}\n\nmessage EpisodeEntry {\n string episode_id = 1;\n string event_type = 2;\n google.protobuf.Timestamp timestamp = 3;\n int64 cycle = 4;\n double importance = 5;\n repeated string tags = 6;\n}\n\n// ── Convergence ────────────────────────────────────────────────────────────\n\nmessage ConvergencePoint {\n int64 cycle = 1;\n google.protobuf.Timestamp timestamp = 2;\n double score = 3;\n double pipeline_ms = 4;\n}\n\n// ── Costs ──────────────────────────────────────────────────────────────────\n\nmessage CostEntry {\n string provider = 1;\n string node_id = 2;\n int64 calls = 3;\n double total_ms = 4;\n double total_cost_usd = 5;\n}\n\n// ── Events (for streaming) ─────────────────────────────────────────────────\n\nmessage EventResponse {\n string type = 1;\n oneof payload {\n SystemHealth health_update = 2;\n Node node_update = 3;\n Issue new_issue = 4;\n ImprovementRecord improvement = 5;\n ActivityEntry activity = 6;\n }\n}\n\n// ── LLM Brain config ───────────────────────────────────────────────────────\n\nmessage BrainConfig {\n string provider = 1; // \"anthropic\", \"openai\", \"google\", \"deepseek\", \"ollama\", \"codex\"\n string model = 2; // e.g. \"claude-sonnet-4-6\", \"gpt-4o\", \"gemini-2.0-flash\"\n double temperature = 3;\n int32 max_tokens = 4;\n string system_prompt = 5;\n map extra_config = 6;\n}\n\nmessage NodeConfig {\n string node_id = 1;\n string node_type = 2;\n BrainConfig brain = 3;\n map parameters = 4;\n string version = 5;\n google.protobuf.Timestamp updated_at = 6;\n}\n\nmessage BrainExecutionEntry {\n string exec_id = 1;\n string node_id = 2;\n string provider = 3;\n string model = 4;\n int64 prompt_tokens = 5;\n int64 completion_tokens = 6;\n double latency_ms = 7;\n bool success = 8;\n string error_text = 9;\n google.protobuf.Timestamp executed_at = 10;\n int64 cycle = 11;\n}\n\nmessage ProviderInfo {\n string id = 1; // \"anthropic\"\n string display_name = 2; // \"Anthropic\"\n repeated string models = 3;\n bool requires_api_key = 4;\n bool available = 5;\n}\n```\n\n- [ ] **Step 4: Write `proto/omega/v1/omega_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\nimport \"omega/v1/types.proto\";\n\nservice OrchestratorService {\n rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);\n rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);\n rpc GetNode(GetNodeRequest) returns (GetNodeResponse);\n rpc ListNodeExecutions(ListNodeExecutionsRequest) returns (ListNodeExecutionsResponse);\n rpc ListTraces(ListTracesRequest) returns (ListTracesResponse);\n rpc GetTrace(GetTraceRequest) returns (GetTraceResponse);\n rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);\n rpc GetMetricsHistory(GetMetricsHistoryRequest) returns (GetMetricsHistoryResponse);\n rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);\n rpc ListActivity(ListActivityRequest) returns (ListActivityResponse);\n rpc ListImprovements(ListImprovementsRequest) returns (ListImprovementsResponse);\n rpc GetNodeMemory(GetNodeMemoryRequest) returns (GetNodeMemoryResponse);\n rpc GetConvergence(GetConvergenceRequest) returns (GetConvergenceResponse);\n rpc GetCosts(GetCostsRequest) returns (GetCostsResponse);\n rpc SubmitFeedback(SubmitFeedbackRequest) returns (SubmitFeedbackResponse);\n rpc StartOrchestrator(StartOrchestratorRequest) returns (StartOrchestratorResponse);\n rpc StopOrchestrator(StopOrchestratorRequest) returns (StopOrchestratorResponse);\n rpc TriggerHeartbeat(TriggerHeartbeatRequest) returns (TriggerHeartbeatResponse);\n rpc StreamEvents(StreamEventsRequest) returns (stream EventResponse);\n\n // ── Brain / LLM config ───────────────────────────────────────────────────\n rpc GetNodeConfig(GetNodeConfigRequest) returns (GetNodeConfigResponse);\n rpc UpdateNodeBrain(UpdateNodeBrainRequest) returns (UpdateNodeBrainResponse);\n rpc ListAvailableProviders(ListAvailableProvidersRequest) returns (ListAvailableProvidersResponse);\n rpc GetBrainExecutionHistory(GetBrainExecutionHistoryRequest) returns (GetBrainExecutionHistoryResponse);\n}\n\n// ── Health ─────────────────────────────────────────────────────────────────\n\nmessage GetHealthRequest {}\nmessage GetHealthResponse { SystemHealth health = 1; }\n\n// ── Nodes ──────────────────────────────────────────────────────────────────\n\nmessage ListNodesRequest {}\nmessage ListNodesResponse { repeated Node nodes = 1; }\n\nmessage GetNodeRequest { string node_id = 1; }\nmessage GetNodeResponse {\n Node node = 1;\n repeated ExecutionRecord recent_executions = 2;\n repeated ImprovementRecord improvements = 3;\n repeated LatencyPoint latency_history = 4;\n}\n\nmessage LatencyPoint {\n google.protobuf.Timestamp ts = 1;\n double duration_ms = 2;\n bool success = 3;\n}\n\nmessage ListNodeExecutionsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListNodeExecutionsResponse { repeated ExecutionRecord executions = 1; }\n\n// ── Traces ─────────────────────────────────────────────────────────────────\n\nmessage ListTracesRequest { int32 limit = 1; }\nmessage ListTracesResponse { repeated TraceSummary traces = 1; }\n\nmessage GetTraceRequest { string trace_id = 1; }\nmessage GetTraceResponse { repeated Span spans = 1; }\n\n// ── Metrics ─────────────────────────────────────────────────────────────────\n\nmessage GetMetricsRequest {}\nmessage GetMetricsResponse {\n SystemHealth system = 1;\n repeated Node nodes = 2;\n repeated CostEntry costs = 3;\n repeated Issue open_issues = 4;\n repeated TraceSummary recent_traces = 5;\n}\n\nmessage GetMetricsHistoryRequest { string node_id = 1; }\nmessage GetMetricsHistoryResponse {\n repeated NodeLatencyHistory node_histories = 1;\n}\nmessage NodeLatencyHistory {\n string node_id = 1;\n string name = 2;\n repeated LatencyPoint history = 3;\n}\n\n// ── Issues ──────────────────────────────────────────────────────────────────\n\nmessage ListIssuesRequest { string state_filter = 1; }\nmessage ListIssuesResponse { repeated Issue issues = 1; }\n\n// ── Activity ─────────────────────────────────────────────────────────────────\n\nmessage ListActivityRequest { int32 limit = 1; }\nmessage ListActivityResponse { repeated ActivityEntry entries = 1; }\n\n// ── Improvements ─────────────────────────────────────────────────────────────\n\nmessage ListImprovementsRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage ListImprovementsResponse { repeated ImprovementRecord improvements = 1; }\n\n// ── Memory ───────────────────────────────────────────────────────────────────\n\nmessage GetNodeMemoryRequest { string node_id = 1; }\nmessage GetNodeMemoryResponse {\n string namespace = 1;\n int64 episodic_count = 2;\n int64 semantic_count = 3;\n repeated SemanticConcept top_semantic = 4;\n repeated EpisodeEntry recent_episodes = 5;\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nmessage GetConvergenceRequest { int32 limit = 1; }\nmessage GetConvergenceResponse { repeated ConvergencePoint points = 1; }\n\n// ── Costs ────────────────────────────────────────────────────────────────────\n\nmessage GetCostsRequest {}\nmessage GetCostsResponse { repeated CostEntry costs = 1; }\n\n// ── Feedback ─────────────────────────────────────────────────────────────────\n\nmessage SubmitFeedbackRequest {\n string text = 1;\n string target_node = 2;\n}\nmessage SubmitFeedbackResponse { bool ok = 1; }\n\n// ── Orchestrator control ─────────────────────────────────────────────────────\n\nmessage StartOrchestratorRequest { int32 heartbeat_secs = 1; }\nmessage StartOrchestratorResponse { bool started = 1; string message = 2; }\n\nmessage StopOrchestratorRequest {}\nmessage StopOrchestratorResponse { bool stopped = 1; string message = 2; }\n\nmessage TriggerHeartbeatRequest {}\nmessage TriggerHeartbeatResponse { bool triggered = 1; string message = 2; }\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nmessage StreamEventsRequest { int32 poll_interval_ms = 1; }\n\n// ── Brain config ──────────────────────────────────────────────────────────────\n\nmessage GetNodeConfigRequest { string node_id = 1; }\nmessage GetNodeConfigResponse { NodeConfig config = 1; }\n\nmessage UpdateNodeBrainRequest {\n string node_id = 1;\n BrainConfig brain = 2;\n}\nmessage UpdateNodeBrainResponse { NodeConfig config = 1; }\n\nmessage ListAvailableProvidersRequest {}\nmessage ListAvailableProvidersResponse { repeated ProviderInfo providers = 1; }\n\nmessage GetBrainExecutionHistoryRequest {\n string node_id = 1;\n int32 limit = 2;\n}\nmessage GetBrainExecutionHistoryResponse { repeated BrainExecutionEntry entries = 1; }\n```\n\n- [ ] **Step 5: Write `proto/omega/v1/node_service.proto`**\n\n```protobuf\nsyntax = \"proto3\";\n\npackage omega.v1;\n\noption go_package = \"github.com/benebsworth/omega/gen/go/omega/v1;omegav1\";\n\n// NodeService — contract for inter-node communication.\n// Python nodes implement this; Go orchestrator calls it.\nservice NodeService {\n rpc Execute(ExecuteRequest) returns (ExecuteResponse);\n rpc Evaluate(EvaluateRequest) returns (EvaluateResponse);\n rpc Improve(ImproveRequest) returns (ImproveResponse);\n rpc GetState(GetStateRequest) returns (GetStateResponse);\n rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);\n}\n\nmessage ExecuteRequest {\n string action = 1;\n map parameters = 2;\n map context = 3;\n}\n\nmessage ExecuteResponse {\n bool success = 1;\n bytes result_json = 2;\n repeated string errors = 3;\n map metrics = 4;\n}\n\nmessage EvaluateRequest {}\nmessage EvaluateResponse { map metrics = 1; }\n\nmessage ImproveRequest { map feedback = 1; }\nmessage ImproveResponse { bool changed = 1; string new_version = 2; }\n\nmessage GetStateRequest {}\nmessage GetStateResponse {\n string node_id = 1;\n string name = 2;\n string version = 3;\n double health = 4;\n string status = 5;\n repeated string capabilities = 6;\n}\n\nmessage GetCapabilitiesRequest {}\nmessage GetCapabilitiesResponse { repeated string capabilities = 1; }\n```\n\n- [ ] **Step 6: Lint and validate protos**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf dep update\nbuf lint\n```\n\nExpected: no errors.\n\n- [ ] **Step 7: Commit protos and buf config**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add buf.yaml buf.gen.yaml proto/\ngit commit -m \"feat(proto): add Omega protobuf service definitions\"\n```\n\n---\n\n## Task 2: Go Module + Code Generation\n\n**Files:**\n- Create: `go.mod`\n- Create: `go.sum` (generated)\n- Create: `gen/go/` (generated)\n- Create: `dashboard/src/gen/` (generated)\n\n- [ ] **Step 1: Initialize Go module**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo mod init github.com/benebsworth/omega\n```\n\n- [ ] **Step 2: Add Go dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo get connectrpc.com/connect@latest\ngo get google.golang.org/protobuf@latest\ngo get modernc.org/sqlite@latest\ngo get golang.org/x/net@latest\n```\n\n- [ ] **Step 3: Run buf generate**\n\n```bash\ncd /Users/benebsworth/projects/omega\nbuf generate\n```\n\nExpected: creates `gen/go/omega/v1/*.pb.go` and `gen/go/omega/v1/*connect*.go`, and `dashboard/src/gen/` with TypeScript files.\n\n- [ ] **Step 4: Verify generated Go compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./gen/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 5: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add go.mod go.sum gen/ dashboard/src/gen/\ngit commit -m \"feat(gen): add Go module and run buf generate for Go + TS stubs\"\n```\n\n---\n\n## Task 3: Go Database Layer\n\n**Files:**\n- Create: `internal/db/db.go`\n\n- [ ] **Step 1: Write `internal/db/db.go`**\n\n```go\n// Package db provides read-only access to the Omega SQLite state and memory databases.\n// The Python node layer writes; this Go layer reads.\npackage db\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t_ \"modernc.org/sqlite\"\n)\n\nconst (\n\tStateDBPath = \"/tmp/omega_victoria_state.db\"\n\tMemoryDBPath = \"/tmp/omega_victoria_memory.db\"\n)\n\n// DB holds connections to both Omega SQLite databases.\ntype DB struct {\n\tstate *sql.DB\n\tmemory *sql.DB\n}\n\n// New opens both databases in WAL mode for concurrent reads.\nfunc New(stateDBPath, memoryDBPath string) (*DB, error) {\n\tstate, err := openDB(stateDBPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open state db: %w\", err)\n\t}\n\tmemory, err := openDB(memoryDBPath)\n\tif err != nil {\n\t\tstate.Close()\n\t\treturn nil, fmt.Errorf(\"open memory db: %w\", err)\n\t}\n\td := &DB{state: state, memory: memory}\n\tif err := d.ensureBrainTables(); err != nil {\n\t\tstate.Close()\n\t\tmemory.Close()\n\t\treturn nil, fmt.Errorf(\"ensure brain tables: %w\", err)\n\t}\n\treturn d, nil\n}\n\nfunc openDB(path string) (*sql.DB, error) {\n\tdb, err := sql.Open(\"sqlite\", path+\"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdb.SetMaxOpenConns(1) // SQLite WAL: one writer, many readers via connection pooling\n\tdb.SetMaxIdleConns(1)\n\treturn db, db.Ping()\n}\n\nfunc (d *DB) Close() {\n\td.state.Close()\n\td.memory.Close()\n}\n\n// ── Node types ──────────────────────────────────────────────────────────────\n\ntype Node struct {\n\tNodeID string\n\tName string\n\tVersion string\n\tCapabilities []string\n\tHealth float64\n\tStatus string\n\tRegisteredAt float64\n\tLastUpdated float64\n\tExecutionsTotal int64\n\tErrorRate float64\n\tAvgLatencyMS float64\n\tP95LatencyMS float64\n\tImprovementCount int64\n\tLastExecution *Execution\n}\n\ntype Execution struct {\n\tExecID string\n\tNodeID string\n\tNodeName string\n\tTraceID string\n\tSpanID string\n\tAction string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tSuccess bool\n\tErrorText string\n\tMetrics map[string]float64\n\tCycle int64\n}\n\ntype LatencyPoint struct {\n\tStartedAt float64\n\tDurationMS float64\n\tSuccess bool\n}\n\n// ── Trace types ─────────────────────────────────────────────────────────────\n\ntype TraceSummary struct {\n\tTraceID string\n\tTraceStarted float64\n\tTraceEnded *float64\n\tTotalDurationMS float64\n\tSpanCount int64\n\tErrorSpans int64\n\tCycle int64\n}\n\ntype Span struct {\n\tSpanID string\n\tTraceID string\n\tParentSpanID string\n\tNodeID string\n\tNodeName string\n\tOperation string\n\tStartedAt float64\n\tEndedAt *float64\n\tDurationMS *float64\n\tStatus string\n\tCycle int64\n}\n\n// ── Issue types ──────────────────────────────────────────────────────────────\n\ntype Issue struct {\n\tIssueID string\n\tDetector string\n\tSeverity string\n\tDescription string\n\tState string\n\tOpenedAt float64\n\tResolvedAt *float64\n\tCycleOpened int64\n\tCycleResolved *int64\n}\n\n// ── Activity types ────────────────────────────────────────────────────────────\n\ntype ActivityEntry struct {\n\tLogID string\n\tActionType string\n\tEntityType string\n\tEntityID string\n\tRecordedAt float64\n\tCycle int64\n}\n\n// ── Improvement types ─────────────────────────────────────────────────────────\n\ntype Improvement struct {\n\tImproveID string\n\tNodeID string\n\tNodeName string\n\tFromVersion string\n\tToVersion string\n\tTriggeredBy string\n\tRecordedAt float64\n\tCycle int64\n\tBeforeMetrics map[string]float64\n\tAfterMetrics map[string]float64\n}\n\n// ── Cost types ────────────────────────────────────────────────────────────────\n\ntype CostEntry struct {\n\tProvider string\n\tNodeID string\n\tCalls int64\n\tTotalMS float64\n\tTotalCostUSD float64\n}\n\n// ── Memory types ──────────────────────────────────────────────────────────────\n\ntype SemanticConcept struct {\n\tConcept string\n\tContent string\n\tConfidence float64\n\tEvidenceCount int64\n\tTags []string\n}\n\ntype EpisodeEntry struct {\n\tEpisodeID string\n\tEventType string\n\tTimestamp float64\n\tCycle int64\n\tImportance float64\n\tTags []string\n}\n\ntype ConvergencePoint struct {\n\tCycle int64\n\tTimestamp float64\n\tScore float64\n\tPipelineMS float64\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\ntype SystemHealth struct {\n\tStatus string\n\tCompositeScore float64\n\tAvgNodeHealth float64\n\tNodeCount int64\n\tOpenIssues int64\n\tErrorIssues int64\n\tUptimeSeconds float64\n\tTotalCycles int64\n}\n\n// ── Query methods ──────────────────────────────────────────────────────────────\n\n// AllNodes returns all registered nodes with computed metrics.\nfunc (d *DB) AllNodes() ([]*Node, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes ORDER BY registered_at`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar nodes []*Node\n\tfor rows.Next() {\n\t\tn := &Node{}\n\t\tvar capsJSON string\n\t\tif err := rows.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\t\tnodes = append(nodes, n)\n\t}\n\n\t// Enrich with computed metrics\n\tfor _, n := range nodes {\n\t\tif err := d.enrichNode(n); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn nodes, nil\n}\n\n// GetNode returns one node by ID, enriched with metrics.\nfunc (d *DB) GetNode(nodeID string) (*Node, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, name, version, capabilities_json, health, status,\n\t\t registered_at, last_updated\n\t\tFROM nodes WHERE node_id = ?`, nodeID)\n\tn := &Node{}\n\tvar capsJSON string\n\tif err := row.Scan(&n.NodeID, &n.Name, &n.Version, &capsJSON,\n\t\t&n.Health, &n.Status, &n.RegisteredAt, &n.LastUpdated); err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(capsJSON), &n.Capabilities) //nolint:errcheck\n\treturn n, d.enrichNode(n)\n}\n\nfunc (d *DB) enrichNode(n *Node) error {\n\t// Execution stats\n\trow := d.state.QueryRow(`\n\t\tSELECT COUNT(*),\n\t\t SUM(CASE WHEN success=0 THEN 1 ELSE 0 END),\n\t\t AVG(duration_ms),\n\t\t MAX(started_at)\n\t\tFROM node_executions WHERE node_id = ?`, n.NodeID)\n\tvar total, failed int64\n\tvar avgMS sql.NullFloat64\n\tvar lastStarted sql.NullFloat64\n\trow.Scan(&total, &failed, &avgMS, &lastStarted) //nolint:errcheck\n\n\tn.ExecutionsTotal = total\n\tif total > 0 {\n\t\tn.ErrorRate = float64(failed) / float64(total)\n\t}\n\tif avgMS.Valid {\n\t\tn.AvgLatencyMS = avgMS.Float64\n\t}\n\n\t// p95 latency — two-query approach (SQLite rejects COUNT(*) inside OFFSET)\n\tvar p95Count int64\n\td.state.QueryRow(`SELECT COUNT(*) FROM node_executions WHERE node_id = ? AND duration_ms IS NOT NULL`, n.NodeID).Scan(&p95Count) //nolint:errcheck\n\tif p95Count > 0 {\n\t\toffset := int64(float64(p95Count) * 0.95)\n\t\tp95row := d.state.QueryRow(`\n\t\t\tSELECT duration_ms FROM node_executions\n\t\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\t\tORDER BY duration_ms\n\t\t\tLIMIT 1 OFFSET ?\n\t\t`, n.NodeID, offset)\n\t\tvar p95 sql.NullFloat64\n\t\tp95row.Scan(&p95) //nolint:errcheck\n\t\tif p95.Valid {\n\t\t\tn.P95LatencyMS = p95.Float64\n\t\t}\n\t}\n\n\t// Improvement count\n\trow2 := d.state.QueryRow(\n\t\t`SELECT COUNT(*) FROM improvement_log WHERE node_id = ?`, n.NodeID)\n\trow2.Scan(&n.ImprovementCount) //nolint:errcheck\n\n\t// Last execution\n\tlastExec, err := d.lastExecution(n.NodeID)\n\tif err == nil {\n\t\tn.LastExecution = lastExec\n\t}\n\treturn nil\n}\n\nfunc (d *DB) lastExecution(nodeID string) (*Execution, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\t action, started_at, ended_at, duration_ms, success,\n\t\t COALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE node_id = ?\n\t\tORDER BY started_at DESC LIMIT 1`, nodeID)\n\treturn scanExecution(row)\n}\n\nfunc scanExecution(row *sql.Row) (*Execution, error) {\n\te := &Execution{}\n\tvar metricsJSON string\n\tvar success int\n\tvar endedAt, durationMS sql.NullFloat64\n\terr := row.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t&e.ErrorText, &metricsJSON, &e.Cycle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\te.Success = success == 1\n\tif endedAt.Valid {\n\t\tv := endedAt.Float64\n\t\te.EndedAt = &v\n\t}\n\tif durationMS.Valid {\n\t\tv := durationMS.Float64\n\t\te.DurationMS = &v\n\t}\n\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\treturn e, nil\n}\n\n// GetExecutions returns recent executions, optionally filtered by node.\nfunc (d *DB) GetExecutions(nodeID string, limit int) ([]*Execution, error) {\n\tquery := `SELECT exec_id, node_id, node_name, COALESCE(trace_id,''), COALESCE(span_id,''),\n\t\taction, started_at, ended_at, duration_ms, success,\n\t\tCOALESCE(error_text,''), metrics_json, cycle\n\t\tFROM node_executions WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY started_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\treturn scanExecutions(rows)\n}\n\nfunc scanExecutions(rows *sql.Rows) ([]*Execution, error) {\n\tvar result []*Execution\n\tfor rows.Next() {\n\t\te := &Execution{}\n\t\tvar metricsJSON string\n\t\tvar success int\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.NodeName, &e.TraceID, &e.SpanID,\n\t\t\t&e.Action, &e.StartedAt, &endedAt, &durationMS, &success,\n\t\t\t&e.ErrorText, &metricsJSON, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\te.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\te.DurationMS = &v\n\t\t}\n\t\tjson.Unmarshal([]byte(metricsJSON), &e.Metrics) //nolint:errcheck\n\t\tresult = append(result, e)\n\t}\n\treturn result, nil\n}\n\n// LatencyHistory returns (started_at, duration_ms, success) for charting.\nfunc (d *DB) LatencyHistory(nodeID string, limit int) ([]*LatencyPoint, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT started_at, duration_ms, success\n\t\tFROM node_executions\n\t\tWHERE node_id = ? AND duration_ms IS NOT NULL\n\t\tORDER BY started_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*LatencyPoint\n\tfor rows.Next() {\n\t\tp := &LatencyPoint{}\n\t\tvar success int\n\t\tif err := rows.Scan(&p.StartedAt, &p.DurationMS, &success); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.Success = success == 1\n\t\tpoints = append(points, p)\n\t}\n\t// Reverse to chronological order\n\tfor i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {\n\t\tpoints[i], points[j] = points[j], points[i]\n\t}\n\treturn points, nil\n}\n\n// RecentTraces returns recent trace summaries.\nfunc (d *DB) RecentTraces(limit int) ([]*TraceSummary, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT trace_id,\n\t\t MIN(started_at) as trace_started,\n\t\t MAX(ended_at) as trace_ended,\n\t\t (MAX(ended_at) - MIN(started_at)) * 1000 as total_duration_ms,\n\t\t COUNT(*) as span_count,\n\t\t SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_spans,\n\t\t MAX(cycle) as cycle\n\t\tFROM traces\n\t\tGROUP BY trace_id\n\t\tORDER BY trace_started DESC\n\t\tLIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar summaries []*TraceSummary\n\tfor rows.Next() {\n\t\tt := &TraceSummary{}\n\t\tvar traceEnded sql.NullFloat64\n\t\tif err := rows.Scan(&t.TraceID, &t.TraceStarted, &traceEnded,\n\t\t\t&t.TotalDurationMS, &t.SpanCount, &t.ErrorSpans, &t.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif traceEnded.Valid {\n\t\t\tv := traceEnded.Float64\n\t\t\tt.TraceEnded = &v\n\t\t}\n\t\tsummaries = append(summaries, t)\n\t}\n\treturn summaries, nil\n}\n\n// GetTraceSpans returns all spans for a trace.\nfunc (d *DB) GetTraceSpans(traceID string) ([]*Span, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT span_id, trace_id, COALESCE(parent_span_id,''),\n\t\t COALESCE(node_id,''), COALESCE(node_name,''),\n\t\t operation, started_at, ended_at, duration_ms, status, cycle\n\t\tFROM traces WHERE trace_id = ? ORDER BY started_at`, traceID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar spans []*Span\n\tfor rows.Next() {\n\t\ts := &Span{}\n\t\tvar endedAt, durationMS sql.NullFloat64\n\t\tif err := rows.Scan(&s.SpanID, &s.TraceID, &s.ParentSpanID,\n\t\t\t&s.NodeID, &s.NodeName, &s.Operation,\n\t\t\t&s.StartedAt, &endedAt, &durationMS, &s.Status, &s.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif endedAt.Valid {\n\t\t\tv := endedAt.Float64\n\t\t\ts.EndedAt = &v\n\t\t}\n\t\tif durationMS.Valid {\n\t\t\tv := durationMS.Float64\n\t\t\ts.DurationMS = &v\n\t\t}\n\t\tspans = append(spans, s)\n\t}\n\treturn spans, nil\n}\n\n// GetIssues returns issues, optionally filtered by state.\nfunc (d *DB) GetIssues(stateFilter string) ([]*Issue, error) {\n\tquery := `SELECT issue_id, detector, severity, description, state,\n\t\topened_at, resolved_at, cycle_opened, cycle_resolved\n\t\tFROM issues WHERE 1=1`\n\targs := []any{}\n\tif stateFilter != \"\" && stateFilter != \"all\" {\n\t\tif stateFilter == \"open\" {\n\t\t\tquery += \" AND state != 'resolved'\"\n\t\t} else {\n\t\t\tquery += \" AND state = ?\"\n\t\t\targs = append(args, stateFilter)\n\t\t}\n\t}\n\tquery += \" ORDER BY opened_at DESC LIMIT 100\"\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar issues []*Issue\n\tfor rows.Next() {\n\t\ti := &Issue{}\n\t\tif err := rows.Scan(&i.IssueID, &i.Detector, &i.Severity, &i.Description,\n\t\t\t&i.State, &i.OpenedAt, &i.ResolvedAt, &i.CycleOpened, &i.CycleResolved); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tissues = append(issues, i)\n\t}\n\treturn issues, nil\n}\n\n// RecentActivity returns recent activity log entries.\nfunc (d *DB) RecentActivity(limit int) ([]*ActivityEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT log_id, action_type, entity_type, entity_id, recorded_at, cycle\n\t\tFROM activity_log ORDER BY recorded_at DESC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*ActivityEntry\n\tfor rows.Next() {\n\t\te := &ActivityEntry{}\n\t\tif err := rows.Scan(&e.LogID, &e.ActionType, &e.EntityType,\n\t\t\t&e.EntityID, &e.RecordedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n\n// GetImprovements returns improvement log, optionally by node.\nfunc (d *DB) GetImprovements(nodeID string, limit int) ([]*Improvement, error) {\n\tquery := `SELECT improve_id, node_id, node_name, from_version, to_version,\n\t\ttriggered_by, recorded_at, cycle, before_metrics_json, after_metrics_json\n\t\tFROM improvement_log WHERE 1=1`\n\targs := []any{}\n\tif nodeID != \"\" {\n\t\tquery += \" AND node_id = ?\"\n\t\targs = append(args, nodeID)\n\t}\n\tquery += \" ORDER BY recorded_at DESC LIMIT ?\"\n\targs = append(args, limit)\n\n\trows, err := d.state.Query(query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar imps []*Improvement\n\tfor rows.Next() {\n\t\timp := &Improvement{}\n\t\tvar beforeJSON, afterJSON string\n\t\tif err := rows.Scan(&imp.ImproveID, &imp.NodeID, &imp.NodeName,\n\t\t\t&imp.FromVersion, &imp.ToVersion, &imp.TriggeredBy,\n\t\t\t&imp.RecordedAt, &imp.Cycle, &beforeJSON, &afterJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(beforeJSON), &imp.BeforeMetrics) //nolint:errcheck\n\t\tjson.Unmarshal([]byte(afterJSON), &imp.AfterMetrics) //nolint:errcheck\n\t\timps = append(imps, imp)\n\t}\n\treturn imps, nil\n}\n\n// GetCosts returns aggregated cost by provider and node.\nfunc (d *DB) GetCosts() ([]*CostEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT provider, node_id,\n\t\t COUNT(*) as calls,\n\t\t SUM(duration_ms) as total_ms,\n\t\t SUM(estimated_cost_usd) as total_cost\n\t\tFROM cost_events GROUP BY provider, node_id`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar costs []*CostEntry\n\tfor rows.Next() {\n\t\tc := &CostEntry{}\n\t\tif err := rows.Scan(&c.Provider, &c.NodeID, &c.Calls, &c.TotalMS, &c.TotalCostUSD); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcosts = append(costs, c)\n\t}\n\treturn costs, nil\n}\n\n// GetConvergence reads cycle_summary episodes from the memory DB.\nfunc (d *DB) GetConvergence(limit int) ([]*ConvergencePoint, error) {\n\trows, err := d.memory.Query(`\n\t\tSELECT cycle, timestamp, content_json\n\t\tFROM episodes WHERE event_type = 'cycle_summary'\n\t\tORDER BY cycle ASC LIMIT ?`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar points []*ConvergencePoint\n\tfor rows.Next() {\n\t\tp := &ConvergencePoint{}\n\t\tvar contentJSON string\n\t\tif err := rows.Scan(&p.Cycle, &p.Timestamp, &contentJSON); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar content map[string]any\n\t\tif err := json.Unmarshal([]byte(contentJSON), &content); err == nil {\n\t\t\tif s, ok := content[\"score\"].(float64); ok {\n\t\t\t\tp.Score = s\n\t\t\t}\n\t\t\tif ms, ok := content[\"pipeline_ms\"].(float64); ok {\n\t\t\t\tp.PipelineMS = ms\n\t\t\t}\n\t\t}\n\t\tpoints = append(points, p)\n\t}\n\treturn points, nil\n}\n\n// GetMemoryStats returns episodic + semantic counts for a namespace.\nfunc (d *DB) GetMemoryStats(namespace string) (int64, int64, []*SemanticConcept, []*EpisodeEntry, error) {\n\tvar epCount, semCount int64\n\n\t// Episodic count\n\tquery := \"SELECT COUNT(*) FROM episodes\"\n\targs := []any{}\n\tif namespace != \"\" {\n\t\tquery += \" WHERE namespace = ?\"\n\t\targs = append(args, namespace)\n\t}\n\td.memory.QueryRow(query, args...).Scan(&epCount) //nolint:errcheck\n\n\t// Semantic count\n\tsemQuery := \"SELECT COUNT(*) FROM semantic_memories\"\n\tsemArgs := []any{}\n\tif namespace != \"\" {\n\t\tsemQuery += \" WHERE namespace = ?\"\n\t\tsemArgs = append(semArgs, namespace)\n\t}\n\td.memory.QueryRow(semQuery, semArgs...).Scan(&semCount) //nolint:errcheck\n\n\t// Top semantic\n\tsemRowQuery := \"SELECT concept, content, confidence, evidence_count, tags_json FROM semantic_memories\"\n\tif namespace != \"\" {\n\t\tsemRowQuery += \" WHERE namespace = ?\"\n\t}\n\tsemRowQuery += \" ORDER BY confidence DESC LIMIT 20\"\n\tsemRows, err := d.memory.Query(semRowQuery, semArgs...)\n\tif err != nil {\n\t\treturn epCount, semCount, nil, nil, err\n\t}\n\tdefer semRows.Close()\n\tvar concepts []*SemanticConcept\n\tfor semRows.Next() {\n\t\tc := &SemanticConcept{}\n\t\tvar tagsJSON string\n\t\tif err := semRows.Scan(&c.Concept, &c.Content, &c.Confidence, &c.EvidenceCount, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, nil, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &c.Tags) //nolint:errcheck\n\t\tconcepts = append(concepts, c)\n\t}\n\n\t// Recent episodes\n\tepRowQuery := \"SELECT episode_id, event_type, timestamp, cycle, importance, tags_json FROM episodes\"\n\tif namespace != \"\" {\n\t\tepRowQuery += \" WHERE namespace = ?\"\n\t}\n\tepRowQuery += \" ORDER BY timestamp DESC LIMIT 20\"\n\tepRows, err := d.memory.Query(epRowQuery, args...)\n\tif err != nil {\n\t\treturn epCount, semCount, concepts, nil, err\n\t}\n\tdefer epRows.Close()\n\tvar episodes []*EpisodeEntry\n\tfor epRows.Next() {\n\t\te := &EpisodeEntry{}\n\t\tvar tagsJSON string\n\t\tif err := epRows.Scan(&e.EpisodeID, &e.EventType, &e.Timestamp, &e.Cycle, &e.Importance, &tagsJSON); err != nil {\n\t\t\treturn epCount, semCount, concepts, nil, err\n\t\t}\n\t\tjson.Unmarshal([]byte(tagsJSON), &e.Tags) //nolint:errcheck\n\t\tepisodes = append(episodes, e)\n\t}\n\n\treturn epCount, semCount, concepts, episodes, nil\n}\n\n// SystemHealth computes overall system health from node states.\nfunc (d *DB) SystemHealth() (*SystemHealth, error) {\n\tnodes, err := d.AllNodes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn &SystemHealth{Status: \"no_nodes\"}, nil\n\t}\n\n\tvar totalHealth float64\n\tvar minHealth float64 = 1.0\n\tfor _, n := range nodes {\n\t\ttotalHealth += n.Health\n\t\tif n.Health < minHealth {\n\t\t\tminHealth = n.Health\n\t\t}\n\t}\n\tavgHealth := totalHealth / float64(len(nodes))\n\n\tissues, _ := d.GetIssues(\"open\")\n\topenCount := int64(len(issues))\n\tvar errorIssues int64\n\tfor _, i := range issues {\n\t\tif i.Severity == \"error\" {\n\t\t\terrorIssues++\n\t\t}\n\t}\n\n\tissuePenalty := float64(errorIssues) * 0.1\n\tif issuePenalty > 0.3 {\n\t\tissuePenalty = 0.3\n\t}\n\tcomposite := avgHealth - issuePenalty\n\tif composite < 0 {\n\t\tcomposite = 0\n\t}\n\n\tstatus := \"healthy\"\n\tif composite < 0.6 {\n\t\tstatus = \"critical\"\n\t} else if composite < 0.8 {\n\t\tstatus = \"degraded\"\n\t}\n\n\t// Uptime from first registered_at\n\tvar minRegAt float64 = float64(time.Now().Unix())\n\tfor _, n := range nodes {\n\t\tif n.RegisteredAt < minRegAt {\n\t\t\tminRegAt = n.RegisteredAt\n\t\t}\n\t}\n\tuptime := float64(time.Now().Unix()) - minRegAt\n\n\t// Total cycles\n\tvar totalCycles int64\n\td.state.QueryRow(\"SELECT COALESCE(MAX(cycle), 0) FROM node_executions\").Scan(&totalCycles) //nolint:errcheck\n\n\treturn &SystemHealth{\n\t\tStatus: status,\n\t\tCompositeScore: round3(composite),\n\t\tAvgNodeHealth: round3(avgHealth),\n\t\tNodeCount: int64(len(nodes)),\n\t\tOpenIssues: openCount,\n\t\tErrorIssues: errorIssues,\n\t\tUptimeSeconds: uptime,\n\t\tTotalCycles: totalCycles,\n\t}, nil\n}\n\nfunc round3(f float64) float64 {\n\treturn float64(int(f*1000+0.5)) / 1000\n}\n\n// ── Brain config types ─────────────────────────────────────────────────────────\n\n// BrainConfig is the LLM adapter config for a node — stored in Go-managed tables.\ntype BrainConfig struct {\n\tNodeID string\n\tProvider string\n\tModel string\n\tTemperature float64\n\tMaxTokens int64\n\tSystemPrompt string\n\tExtraConfig map[string]string\n\tUpdatedAt float64\n}\n\ntype BrainExecutionEntry struct {\n\tExecID string\n\tNodeID string\n\tProvider string\n\tModel string\n\tPromptTokens int64\n\tCompletionTokens int64\n\tLatencyMS float64\n\tSuccess bool\n\tErrorText string\n\tExecutedAt float64\n\tCycle int64\n}\n\n// ── Brain config DB methods ────────────────────────────────────────────────────\n\n// ensureBrainTables creates the Go-managed brain config tables if they don't exist.\n// Call this in New() before returning.\nfunc (d *DB) ensureBrainTables() error {\n\t_, err := d.state.Exec(`\n\t\tCREATE TABLE IF NOT EXISTS node_brain_config (\n\t\t\tnode_id TEXT PRIMARY KEY,\n\t\t\tprovider TEXT NOT NULL DEFAULT 'anthropic',\n\t\t\tmodel TEXT NOT NULL DEFAULT 'claude-sonnet-4-6',\n\t\t\ttemperature REAL NOT NULL DEFAULT 0.7,\n\t\t\tmax_tokens INTEGER NOT NULL DEFAULT 4096,\n\t\t\tsystem_prompt TEXT NOT NULL DEFAULT '',\n\t\t\textra_config TEXT NOT NULL DEFAULT '{}',\n\t\t\tupdated_at REAL NOT NULL DEFAULT (unixepoch('now'))\n\t\t);\n\t\tCREATE TABLE IF NOT EXISTS brain_execution_log (\n\t\t\texec_id TEXT PRIMARY KEY,\n\t\t\tnode_id TEXT NOT NULL,\n\t\t\tprovider TEXT NOT NULL,\n\t\t\tmodel TEXT NOT NULL,\n\t\t\tprompt_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tcompletion_tokens INTEGER NOT NULL DEFAULT 0,\n\t\t\tlatency_ms REAL NOT NULL DEFAULT 0,\n\t\t\tsuccess INTEGER NOT NULL DEFAULT 1,\n\t\t\terror_text TEXT NOT NULL DEFAULT '',\n\t\t\texecuted_at REAL NOT NULL DEFAULT (unixepoch('now')),\n\t\t\tcycle INTEGER NOT NULL DEFAULT 0\n\t\t);`)\n\treturn err\n}\n\n// GetBrainConfig returns the brain config for a node (defaults if not set).\nfunc (d *DB) GetBrainConfig(nodeID string) (*BrainConfig, error) {\n\trow := d.state.QueryRow(`\n\t\tSELECT node_id, provider, model, temperature, max_tokens,\n\t\t system_prompt, extra_config, updated_at\n\t\tFROM node_brain_config WHERE node_id = ?`, nodeID)\n\tc := &BrainConfig{}\n\tvar extraJSON string\n\terr := row.Scan(&c.NodeID, &c.Provider, &c.Model, &c.Temperature,\n\t\t&c.MaxTokens, &c.SystemPrompt, &extraJSON, &c.UpdatedAt)\n\tif err == sql.ErrNoRows {\n\t\t// Return defaults\n\t\treturn &BrainConfig{\n\t\t\tNodeID: nodeID,\n\t\t\tProvider: \"anthropic\",\n\t\t\tModel: \"claude-sonnet-4-6\",\n\t\t\tTemperature: 0.7,\n\t\t\tMaxTokens: 4096,\n\t\t}, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjson.Unmarshal([]byte(extraJSON), &c.ExtraConfig) //nolint:errcheck\n\treturn c, nil\n}\n\n// SetBrainConfig upserts the brain config for a node.\nfunc (d *DB) SetBrainConfig(c *BrainConfig) error {\n\textraJSON, _ := json.Marshal(c.ExtraConfig)\n\t_, err := d.state.Exec(`\n\t\tINSERT INTO node_brain_config\n\t\t\t(node_id, provider, model, temperature, max_tokens, system_prompt, extra_config, updated_at)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, unixepoch('now'))\n\t\tON CONFLICT(node_id) DO UPDATE SET\n\t\t\tprovider=excluded.provider, model=excluded.model,\n\t\t\ttemperature=excluded.temperature, max_tokens=excluded.max_tokens,\n\t\t\tsystem_prompt=excluded.system_prompt, extra_config=excluded.extra_config,\n\t\t\tupdated_at=excluded.updated_at`,\n\t\tc.NodeID, c.Provider, c.Model, c.Temperature,\n\t\tc.MaxTokens, c.SystemPrompt, string(extraJSON))\n\treturn err\n}\n\n// GetBrainHistory returns recent brain execution log for a node.\nfunc (d *DB) GetBrainHistory(nodeID string, limit int) ([]*BrainExecutionEntry, error) {\n\trows, err := d.state.Query(`\n\t\tSELECT exec_id, node_id, provider, model, prompt_tokens, completion_tokens,\n\t\t latency_ms, success, error_text, executed_at, cycle\n\t\tFROM brain_execution_log WHERE node_id = ?\n\t\tORDER BY executed_at DESC LIMIT ?`, nodeID, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar entries []*BrainExecutionEntry\n\tfor rows.Next() {\n\t\te := &BrainExecutionEntry{}\n\t\tvar success int\n\t\tif err := rows.Scan(&e.ExecID, &e.NodeID, &e.Provider, &e.Model,\n\t\t\t&e.PromptTokens, &e.CompletionTokens, &e.LatencyMS,\n\t\t\t&success, &e.ErrorText, &e.ExecutedAt, &e.Cycle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\te.Success = success == 1\n\t\tentries = append(entries, e)\n\t}\n\treturn entries, nil\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/db/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/ go.mod go.sum\ngit commit -m \"feat(db): add Go SQLite read layer for Omega state + memory DBs\"\n```\n\n---\n\n## Task 4: Go Connect-RPC Handler\n\n**Files:**\n- Create: `internal/handler/orchestrator.go`\n\n- [ ] **Step 1: Write `internal/handler/orchestrator.go`**\n\n```go\n// Package handler implements the OrchestratorService Connect-RPC handler.\npackage handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"time\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n)\n\n// Ensure interface satisfaction at compile time.\n// Buf's connectrpc/go plugin generates the handler interface in the same package as the proto types.\nvar _ omegav1.OrchestratorServiceHandler = (*OrchestratorHandler)(nil)\n\n// OrchestratorHandler implements OrchestratorService.\ntype OrchestratorHandler struct {\n\tdb *db.DB\n\tmu sync.Mutex\n\torchCmd *exec.Cmd\n\torchRunning bool\n}\n\n// New creates an OrchestratorHandler backed by the given DB.\nfunc New(database *db.DB) *OrchestratorHandler {\n\treturn &OrchestratorHandler{db: database}\n}\n\n// tsFromUnix converts a float64 Unix timestamp to a protobuf Timestamp.\nfunc tsFromUnix(u float64) *timestamppb.Timestamp {\n\tif u == 0 {\n\t\treturn nil\n\t}\n\tsec := int64(u)\n\tnsec := int32((u - float64(sec)) * 1e9)\n\treturn timestamppb.New(time.Unix(sec, int64(nsec)))\n}\n\nfunc tsFromUnixPtr(u *float64) *timestamppb.Timestamp {\n\tif u == nil {\n\t\treturn nil\n\t}\n\treturn tsFromUnix(*u)\n}\n\n// ── Health ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetHealth(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetHealthRequest],\n) (*connect.Response[omegav1.GetHealthResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetHealthResponse{\n\t\tHealth: dbHealthToProto(health),\n\t}), nil\n}\n\nfunc dbHealthToProto(h *db.SystemHealth) *omegav1.SystemHealth {\n\treturn &omegav1.SystemHealth{\n\t\tStatus: h.Status,\n\t\tCompositeScore: h.CompositeScore,\n\t\tAvgNodeHealth: h.AvgNodeHealth,\n\t\tNodeCount: h.NodeCount,\n\t\tOpenIssues: h.OpenIssues,\n\t\tErrorIssues: h.ErrorIssues,\n\t\tUptimeSeconds: h.UptimeSeconds,\n\t\tTotalCycles: h.TotalCycles,\n\t}\n}\n\n// ── Nodes ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListNodes(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodesRequest],\n) (*connect.Response[omegav1.ListNodesResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoNodes []*omegav1.Node\n\tfor _, n := range nodes {\n\t\tprotoNodes = append(protoNodes, dbNodeToProto(n))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodesResponse{Nodes: protoNodes}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNode(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeRequest],\n) (*connect.Response[omegav1.GetNodeResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\texecs, _ := h.db.GetExecutions(req.Msg.NodeId, 10)\n\timps, _ := h.db.GetImprovements(req.Msg.NodeId, 20)\n\thistory, _ := h.db.LatencyHistory(req.Msg.NodeId, 100)\n\n\tresp := &omegav1.GetNodeResponse{\n\t\tNode: dbNodeToProto(n),\n\t\tImprovements: dbImpsToProto(imps),\n\t}\n\tfor _, e := range execs {\n\t\tresp.RecentExecutions = append(resp.RecentExecutions, dbExecToProto(e))\n\t}\n\tfor _, p := range history {\n\t\tresp.LatencyHistory = append(resp.LatencyHistory, &omegav1.LatencyPoint{\n\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\tDurationMs: p.DurationMS,\n\t\t\tSuccess: p.Success,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) ListNodeExecutions(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListNodeExecutionsRequest],\n) (*connect.Response[omegav1.ListNodeExecutionsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\texecs, err := h.db.GetExecutions(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoExecs []*omegav1.ExecutionRecord\n\tfor _, e := range execs {\n\t\tprotoExecs = append(protoExecs, dbExecToProto(e))\n\t}\n\treturn connect.NewResponse(&omegav1.ListNodeExecutionsResponse{Executions: protoExecs}), nil\n}\n\nfunc dbNodeToProto(n *db.Node) *omegav1.Node {\n\tnode := &omegav1.Node{\n\t\tNodeId: n.NodeID,\n\t\tName: n.Name,\n\t\tVersion: n.Version,\n\t\tCapabilities: n.Capabilities,\n\t\tHealth: n.Health,\n\t\tStatus: n.Status,\n\t\tRegisteredAt: tsFromUnix(n.RegisteredAt),\n\t\tLastUpdated: tsFromUnix(n.LastUpdated),\n\t\tExecutionsTotal: n.ExecutionsTotal,\n\t\tErrorRate: n.ErrorRate,\n\t\tAvgLatencyMs: n.AvgLatencyMS,\n\t\tP95LatencyMs: n.P95LatencyMS,\n\t\tImprovementCount: n.ImprovementCount,\n\t}\n\tif n.LastExecution != nil {\n\t\tnode.LastExecution = dbExecToProto(n.LastExecution)\n\t}\n\treturn node\n}\n\nfunc dbExecToProto(e *db.Execution) *omegav1.ExecutionRecord {\n\trec := &omegav1.ExecutionRecord{\n\t\tExecId: e.ExecID,\n\t\tNodeId: e.NodeID,\n\t\tNodeName: e.NodeName,\n\t\tTraceId: e.TraceID,\n\t\tSpanId: e.SpanID,\n\t\tAction: e.Action,\n\t\tStartedAt: tsFromUnix(e.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(e.EndedAt),\n\t\tSuccess: e.Success,\n\t\tErrorText: e.ErrorText,\n\t\tMetrics: e.Metrics,\n\t\tCycle: e.Cycle,\n\t}\n\tif e.DurationMS != nil {\n\t\trec.DurationMs = *e.DurationMS\n\t}\n\treturn rec\n}\n\n// ── Traces ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListTraces(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListTracesRequest],\n) (*connect.Response[omegav1.ListTracesResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 20\n\t}\n\ttraces, err := h.db.RecentTraces(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoTraces []*omegav1.TraceSummary\n\tfor _, t := range traces {\n\t\tprotoTraces = append(protoTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(&omegav1.ListTracesResponse{Traces: protoTraces}), nil\n}\n\nfunc (h *OrchestratorHandler) GetTrace(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetTraceRequest],\n) (*connect.Response[omegav1.GetTraceResponse], error) {\n\tspans, err := h.db.GetTraceSpans(req.Msg.TraceId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tif len(spans) == 0 {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"trace not found: %s\", req.Msg.TraceId))\n\t}\n\tvar protoSpans []*omegav1.Span\n\tfor _, s := range spans {\n\t\tprotoSpans = append(protoSpans, dbSpanToProto(s))\n\t}\n\treturn connect.NewResponse(&omegav1.GetTraceResponse{Spans: protoSpans}), nil\n}\n\nfunc dbTraceSummaryToProto(t *db.TraceSummary) *omegav1.TraceSummary {\n\treturn &omegav1.TraceSummary{\n\t\tTraceId: t.TraceID,\n\t\tTraceStarted: tsFromUnix(t.TraceStarted),\n\t\tTraceEnded: tsFromUnixPtr(t.TraceEnded),\n\t\tTotalDurationMs: t.TotalDurationMS,\n\t\tSpanCount: t.SpanCount,\n\t\tErrorSpans: t.ErrorSpans,\n\t\tCycle: t.Cycle,\n\t}\n}\n\nfunc dbSpanToProto(s *db.Span) *omegav1.Span {\n\tspan := &omegav1.Span{\n\t\tSpanId: s.SpanID,\n\t\tTraceId: s.TraceID,\n\t\tParentSpanId: s.ParentSpanID,\n\t\tNodeId: s.NodeID,\n\t\tNodeName: s.NodeName,\n\t\tOperation: s.Operation,\n\t\tStartedAt: tsFromUnix(s.StartedAt),\n\t\tEndedAt: tsFromUnixPtr(s.EndedAt),\n\t\tStatus: s.Status,\n\t\tCycle: s.Cycle,\n\t}\n\tif s.DurationMS != nil {\n\t\tspan.DurationMs = *s.DurationMS\n\t}\n\treturn span\n}\n\n// ── Metrics ───────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetMetrics(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsRequest],\n) (*connect.Response[omegav1.GetMetricsResponse], error) {\n\thealth, err := h.db.SystemHealth()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tcosts, _ := h.db.GetCosts()\n\tissues, _ := h.db.GetIssues(\"open\")\n\ttraces, _ := h.db.RecentTraces(5)\n\n\tresp := &omegav1.GetMetricsResponse{System: dbHealthToProto(health)}\n\tfor _, n := range nodes {\n\t\tresp.Nodes = append(resp.Nodes, dbNodeToProto(n))\n\t}\n\tfor _, c := range costs {\n\t\tresp.Costs = append(resp.Costs, dbCostToProto(c))\n\t}\n\tfor _, i := range issues {\n\t\tresp.OpenIssues = append(resp.OpenIssues, dbIssueToProto(i))\n\t}\n\tfor _, t := range traces {\n\t\tresp.RecentTraces = append(resp.RecentTraces, dbTraceSummaryToProto(t))\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (h *OrchestratorHandler) GetMetricsHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetMetricsHistoryRequest],\n) (*connect.Response[omegav1.GetMetricsHistoryResponse], error) {\n\tnodes, err := h.db.AllNodes()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar histories []*omegav1.NodeLatencyHistory\n\tfor _, n := range nodes {\n\t\tif req.Msg.NodeId != \"\" && n.NodeID != req.Msg.NodeId {\n\t\t\tcontinue\n\t\t}\n\t\thistory, _ := h.db.LatencyHistory(n.NodeID, 100)\n\t\tnh := &omegav1.NodeLatencyHistory{NodeId: n.NodeID, Name: n.Name}\n\t\tfor _, p := range history {\n\t\t\tnh.History = append(nh.History, &omegav1.LatencyPoint{\n\t\t\t\tTs: tsFromUnix(p.StartedAt),\n\t\t\t\tDurationMs: p.DurationMS,\n\t\t\t\tSuccess: p.Success,\n\t\t\t})\n\t\t}\n\t\thistories = append(histories, nh)\n\t}\n\treturn connect.NewResponse(&omegav1.GetMetricsHistoryResponse{NodeHistories: histories}), nil\n}\n\n// ── Issues ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListIssues(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListIssuesRequest],\n) (*connect.Response[omegav1.ListIssuesResponse], error) {\n\tissues, err := h.db.GetIssues(req.Msg.StateFilter)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoIssues []*omegav1.Issue\n\tfor _, i := range issues {\n\t\tprotoIssues = append(protoIssues, dbIssueToProto(i))\n\t}\n\treturn connect.NewResponse(&omegav1.ListIssuesResponse{Issues: protoIssues}), nil\n}\n\nfunc dbIssueToProto(i *db.Issue) *omegav1.Issue {\n\tissue := &omegav1.Issue{\n\t\tIssueId: i.IssueID,\n\t\tDetector: i.Detector,\n\t\tSeverity: i.Severity,\n\t\tDescription: i.Description,\n\t\tState: i.State,\n\t\tOpenedAt: tsFromUnix(i.OpenedAt),\n\t\tResolvedAt: tsFromUnixPtr(i.ResolvedAt),\n\t\tCycleOpened: i.CycleOpened,\n\t}\n\tif i.CycleResolved != nil {\n\t\tissue.CycleResolved = *i.CycleResolved\n\t}\n\treturn issue\n}\n\n// ── Activity ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListActivity(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListActivityRequest],\n) (*connect.Response[omegav1.ListActivityResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.RecentActivity(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.ActivityEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.ActivityEntry{\n\t\t\tLogId: e.LogID,\n\t\t\tActionType: e.ActionType,\n\t\t\tEntityType: e.EntityType,\n\t\t\tEntityId: e.EntityID,\n\t\t\tRecordedAt: tsFromUnix(e.RecordedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.ListActivityResponse{Entries: protoEntries}), nil\n}\n\n// ── Improvements ──────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) ListImprovements(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListImprovementsRequest],\n) (*connect.Response[omegav1.ListImprovementsResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\timps, err := h.db.GetImprovements(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.ListImprovementsResponse{\n\t\tImprovements: dbImpsToProto(imps),\n\t}), nil\n}\n\nfunc dbImpsToProto(imps []*db.Improvement) []*omegav1.ImprovementRecord {\n\tvar result []*omegav1.ImprovementRecord\n\tfor _, imp := range imps {\n\t\tresult = append(result, &omegav1.ImprovementRecord{\n\t\t\tImproveId: imp.ImproveID,\n\t\t\tNodeId: imp.NodeID,\n\t\t\tNodeName: imp.NodeName,\n\t\t\tFromVersion: imp.FromVersion,\n\t\t\tToVersion: imp.ToVersion,\n\t\t\tTriggeredBy: imp.TriggeredBy,\n\t\t\tRecordedAt: tsFromUnix(imp.RecordedAt),\n\t\t\tCycle: imp.Cycle,\n\t\t\tBeforeMetrics: imp.BeforeMetrics,\n\t\t\tAfterMetrics: imp.AfterMetrics,\n\t\t})\n\t}\n\treturn result\n}\n\n// ── Memory ────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetNodeMemory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeMemoryRequest],\n) (*connect.Response[omegav1.GetNodeMemoryResponse], error) {\n\t// Resolve node name for namespace\n\tvar namespace string\n\tif req.Msg.NodeId != \"\" {\n\t\tn, err := h.db.GetNode(req.Msg.NodeId)\n\t\tif err != nil {\n\t\t\treturn nil, connect.NewError(connect.CodeNotFound, err)\n\t\t}\n\t\tnamespace = n.Name\n\t}\n\n\tepCount, semCount, concepts, episodes, err := h.db.GetMemoryStats(namespace)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\n\tresp := &omegav1.GetNodeMemoryResponse{\n\t\tNamespace: namespace,\n\t\tEpisodicCount: epCount,\n\t\tSemanticCount: semCount,\n\t}\n\tfor _, c := range concepts {\n\t\tresp.TopSemantic = append(resp.TopSemantic, &omegav1.SemanticConcept{\n\t\t\tConcept: c.Concept,\n\t\t\tContent: c.Content,\n\t\t\tConfidence: c.Confidence,\n\t\t\tEvidenceCount: c.EvidenceCount,\n\t\t\tTags: c.Tags,\n\t\t})\n\t}\n\tfor _, e := range episodes {\n\t\tresp.RecentEpisodes = append(resp.RecentEpisodes, &omegav1.EpisodeEntry{\n\t\t\tEpisodeId: e.EpisodeID,\n\t\t\tEventType: e.EventType,\n\t\t\tTimestamp: tsFromUnix(e.Timestamp),\n\t\t\tCycle: e.Cycle,\n\t\t\tImportance: e.Importance,\n\t\t\tTags: e.Tags,\n\t\t})\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// ── Convergence ───────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetConvergence(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetConvergenceRequest],\n) (*connect.Response[omegav1.GetConvergenceResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 100\n\t}\n\tpoints, err := h.db.GetConvergence(limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoPoints []*omegav1.ConvergencePoint\n\tfor _, p := range points {\n\t\tprotoPoints = append(protoPoints, &omegav1.ConvergencePoint{\n\t\t\tCycle: p.Cycle,\n\t\t\tTimestamp: tsFromUnix(p.Timestamp),\n\t\t\tScore: p.Score,\n\t\t\tPipelineMs: p.PipelineMS,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetConvergenceResponse{Points: protoPoints}), nil\n}\n\n// ── Costs ─────────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) GetCosts(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetCostsRequest],\n) (*connect.Response[omegav1.GetCostsResponse], error) {\n\tcosts, err := h.db.GetCosts()\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoCosts []*omegav1.CostEntry\n\tfor _, c := range costs {\n\t\tprotoCosts = append(protoCosts, dbCostToProto(c))\n\t}\n\treturn connect.NewResponse(&omegav1.GetCostsResponse{Costs: protoCosts}), nil\n}\n\nfunc dbCostToProto(c *db.CostEntry) *omegav1.CostEntry {\n\treturn &omegav1.CostEntry{\n\t\tProvider: c.Provider,\n\t\tNodeId: c.NodeID,\n\t\tCalls: c.Calls,\n\t\tTotalMs: c.TotalMS,\n\t\tTotalCostUsd: c.TotalCostUSD,\n\t}\n}\n\n// ── Feedback ──────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) SubmitFeedback(\n\tctx context.Context,\n\treq *connect.Request[omegav1.SubmitFeedbackRequest],\n) (*connect.Response[omegav1.SubmitFeedbackResponse], error) {\n\t// Shell out to Python to store feedback in MemoryKernel\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-c\", fmt.Sprintf(`\nfrom omega.core.memory import MemoryKernel\nm = MemoryKernel(db_path='/tmp/omega_victoria_memory.db')\nm.store_episode('human_feedback', {'text': %q, 'target_node': %q}, tags=['feedback','human'], importance=0.9)\nprint('ok')\n`, req.Msg.Text, req.Msg.TargetNode))\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"feedback storage failed: %w\", err))\n\t}\n\treturn connect.NewResponse(&omegav1.SubmitFeedbackResponse{Ok: true}), nil\n}\n\n// ── Orchestrator control ──────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StartOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StartOrchestratorRequest],\n) (*connect.Response[omegav1.StartOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif h.orchRunning {\n\t\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\t\tStarted: false, Message: \"already running\",\n\t\t}), nil\n\t}\n\theartbeat := req.Msg.HeartbeatSecs\n\tif heartbeat <= 0 {\n\t\theartbeat = 120\n\t}\n\tcmd := exec.Command(\"python3\", \"-m\", \"omega.examples.victoria_main\",\n\t\t\"--heartbeat\", fmt.Sprintf(\"%d\", heartbeat))\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, fmt.Errorf(\"start orchestrator: %w\", err))\n\t}\n\th.orchCmd = cmd\n\th.orchRunning = true\n\t// Watch for exit\n\tgo func() {\n\t\tcmd.Wait() //nolint:errcheck\n\t\th.mu.Lock()\n\t\th.orchRunning = false\n\t\th.orchCmd = nil\n\t\th.mu.Unlock()\n\t}()\n\treturn connect.NewResponse(&omegav1.StartOrchestratorResponse{\n\t\tStarted: true, Message: \"started\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) StopOrchestrator(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StopOrchestratorRequest],\n) (*connect.Response[omegav1.StopOrchestratorResponse], error) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif !h.orchRunning || h.orchCmd == nil {\n\t\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\t\tStopped: false, Message: \"not running\",\n\t\t}), nil\n\t}\n\tif err := h.orchCmd.Process.Kill(); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\th.orchRunning = false\n\th.orchCmd = nil\n\treturn connect.NewResponse(&omegav1.StopOrchestratorResponse{\n\t\tStopped: true, Message: \"stopped\",\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) TriggerHeartbeat(\n\tctx context.Context,\n\treq *connect.Request[omegav1.TriggerHeartbeatRequest],\n) (*connect.Response[omegav1.TriggerHeartbeatResponse], error) {\n\tcmd := exec.CommandContext(ctx, \"python3\", \"-m\", \"omega.examples.victoria_main\", \"--once\")\n\tgo cmd.Run() //nolint:errcheck\n\treturn connect.NewResponse(&omegav1.TriggerHeartbeatResponse{\n\t\tTriggered: true, Message: \"triggered single heartbeat\",\n\t}), nil\n}\n\n// ── Streaming ─────────────────────────────────────────────────────────────────\n\nfunc (h *OrchestratorHandler) StreamEvents(\n\tctx context.Context,\n\treq *connect.Request[omegav1.StreamEventsRequest],\n\tstream *connect.ServerStream[omegav1.EventResponse],\n) error {\n\tinterval := time.Duration(req.Msg.PollIntervalMs) * time.Millisecond\n\tif interval < 500*time.Millisecond {\n\t\tinterval = 2 * time.Second\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase <-ticker.C:\n\t\t\thealth, err := h.db.SystemHealth()\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := stream.Send(&omegav1.EventResponse{\n\t\t\t\tType: \"health_update\",\n\t\t\t\tPayload: &omegav1.EventResponse_HealthUpdate{\n\t\t\t\t\tHealthUpdate: dbHealthToProto(health),\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ── Brain config RPCs ─────────────────────────────────────────────────────────\n\n// knownProviders lists all supported LLM providers and their available models.\nvar knownProviders = []*omegav1.ProviderInfo{\n\t{Id: \"anthropic\", DisplayName: \"Anthropic\", Models: []string{\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"openai\", DisplayName: \"OpenAI\", Models: []string{\"gpt-4o\", \"gpt-4o-mini\", \"o1\", \"o3-mini\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"google\", DisplayName: \"Google\", Models: []string{\"gemini-2.0-flash\", \"gemini-2.0-pro\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"deepseek\", DisplayName: \"DeepSeek\", Models: []string{\"deepseek-chat\", \"deepseek-reasoner\"}, RequiresApiKey: true, Available: true},\n\t{Id: \"ollama\", DisplayName: \"Ollama (local)\", Models: []string{\"llama3.3\", \"qwen2.5-coder\", \"mistral\"}, RequiresApiKey: false, Available: true},\n}\n\nfunc (h *OrchestratorHandler) ListAvailableProviders(\n\tctx context.Context,\n\treq *connect.Request[omegav1.ListAvailableProvidersRequest],\n) (*connect.Response[omegav1.ListAvailableProvidersResponse], error) {\n\treturn connect.NewResponse(&omegav1.ListAvailableProvidersResponse{\n\t\tProviders: knownProviders,\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetNodeConfig(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetNodeConfigRequest],\n) (*connect.Response[omegav1.GetNodeConfigResponse], error) {\n\tn, err := h.db.GetNode(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"node not found: %s\", req.Msg.NodeId))\n\t}\n\tbrain, err := h.db.GetBrainConfig(req.Msg.NodeId)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\treturn connect.NewResponse(&omegav1.GetNodeConfigResponse{\n\t\tConfig: dbNodeConfigToProto(n, brain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) UpdateNodeBrain(\n\tctx context.Context,\n\treq *connect.Request[omegav1.UpdateNodeBrainRequest],\n) (*connect.Response[omegav1.UpdateNodeBrainResponse], error) {\n\tbrain := &db.BrainConfig{\n\t\tNodeID: req.Msg.NodeId,\n\t\tProvider: req.Msg.Brain.Provider,\n\t\tModel: req.Msg.Brain.Model,\n\t\tTemperature: req.Msg.Brain.Temperature,\n\t\tMaxTokens: int64(req.Msg.Brain.MaxTokens),\n\t\tSystemPrompt: req.Msg.Brain.SystemPrompt,\n\t\tExtraConfig: req.Msg.Brain.ExtraConfig,\n\t}\n\tif err := h.db.SetBrainConfig(brain); err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tn, _ := h.db.GetNode(req.Msg.NodeId)\n\tupdatedBrain, _ := h.db.GetBrainConfig(req.Msg.NodeId)\n\treturn connect.NewResponse(&omegav1.UpdateNodeBrainResponse{\n\t\tConfig: dbNodeConfigToProto(n, updatedBrain),\n\t}), nil\n}\n\nfunc (h *OrchestratorHandler) GetBrainExecutionHistory(\n\tctx context.Context,\n\treq *connect.Request[omegav1.GetBrainExecutionHistoryRequest],\n) (*connect.Response[omegav1.GetBrainExecutionHistoryResponse], error) {\n\tlimit := int(req.Msg.Limit)\n\tif limit <= 0 {\n\t\tlimit = 50\n\t}\n\tentries, err := h.db.GetBrainHistory(req.Msg.NodeId, limit)\n\tif err != nil {\n\t\treturn nil, connect.NewError(connect.CodeInternal, err)\n\t}\n\tvar protoEntries []*omegav1.BrainExecutionEntry\n\tfor _, e := range entries {\n\t\tprotoEntries = append(protoEntries, &omegav1.BrainExecutionEntry{\n\t\t\tExecId: e.ExecID,\n\t\t\tNodeId: e.NodeID,\n\t\t\tProvider: e.Provider,\n\t\t\tModel: e.Model,\n\t\t\tPromptTokens: e.PromptTokens,\n\t\t\tCompletionTokens: e.CompletionTokens,\n\t\t\tLatencyMs: e.LatencyMS,\n\t\t\tSuccess: e.Success,\n\t\t\tErrorText: e.ErrorText,\n\t\t\tExecutedAt: tsFromUnix(e.ExecutedAt),\n\t\t\tCycle: e.Cycle,\n\t\t})\n\t}\n\treturn connect.NewResponse(&omegav1.GetBrainExecutionHistoryResponse{Entries: protoEntries}), nil\n}\n\nfunc dbNodeConfigToProto(n *db.Node, brain *db.BrainConfig) *omegav1.NodeConfig {\n\tconfig := &omegav1.NodeConfig{\n\t\tNodeId: n.NodeID,\n\t\tNodeType: n.Name,\n\t\tVersion: n.Version,\n\t\tBrain: &omegav1.BrainConfig{\n\t\t\tProvider: brain.Provider,\n\t\t\tModel: brain.Model,\n\t\t\tTemperature: brain.Temperature,\n\t\t\tMaxTokens: int32(brain.MaxTokens),\n\t\t\tSystemPrompt: brain.SystemPrompt,\n\t\t\tExtraConfig: brain.ExtraConfig,\n\t\t},\n\t\tUpdatedAt: tsFromUnix(brain.UpdatedAt),\n\t}\n\treturn config\n}\n```\n\n- [ ] **Step 2: Verify it compiles**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./internal/handler/...\n```\n\nExpected: no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add internal/handler/\ngit commit -m \"feat(handler): implement OrchestratorService + brain config Connect-RPC handlers\"\n```\n\n---\n\n## Task 5: Go API Server Entry Point\n\n**Files:**\n- Create: `cmd/omega-api/main.go`\n\n- [ ] **Step 1: Write `cmd/omega-api/main.go`**\n\n```go\n// Command omega-api serves the Omega dashboard Connect-RPC API.\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"connectrpc.com/connect\"\n\tconnectcors \"connectrpc.com/connect/cors\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\tomegav1 \"github.com/benebsworth/omega/gen/go/omega/v1\"\n\t\"github.com/benebsworth/omega/internal/db\"\n\t\"github.com/benebsworth/omega/internal/handler\"\n)\n\nfunc main() {\n\tdatabase, err := db.New(db.StateDBPath, db.MemoryDBPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"open DB: %v\", err)\n\t}\n\tdefer database.Close()\n\n\th := handler.New(database)\n\tmux := http.NewServeMux()\n\n\t// Buf's connectrpc/go plugin generates NewOrchestratorServiceHandler in the same package.\n\tpath, svcHandler := omegav1.NewOrchestratorServiceHandler(h,\n\t\tconnect.WithCompressMinBytes(1024),\n\t)\n\tmux.Handle(path, svcHandler)\n\n\t// CORS — use connectrpc.com/connect/cors helpers to include streaming headers.\n\tcorsHandler := withCORS(mux)\n\n\taddr := \":8080\"\n\tlog.Printf(\"Omega API listening on %s (Connect-RPC)\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, h2c.NewHandler(corsHandler, &http2.Server{})))\n}\n\nfunc withCORS(h http.Handler) http.Handler {\n\tallowedHeaders := append(connectcors.AllowedHeaders(), \"X-User-Agent\")\n\texposedHeaders := connectcors.ExposedHeaders()\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", strings.Join(allowedHeaders, \", \"))\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", strings.Join(exposedHeaders, \", \"))\n\t\tif r.Method == http.MethodOptions {\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\treturn\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t})\n}\n```\n\n- [ ] **Step 2: Build and verify**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api/...\n```\n\nExpected: produces `./omega-api` (or similar) without errors.\n\n- [ ] **Step 3: Smoke test — start server and verify it responds**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\ncurl -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" \\\n -d '{}' | python3 -m json.tool\nkill %1\n```\n\nExpected: JSON response with health data (or empty `{}` if DB doesn't exist yet).\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add cmd/\ngit commit -m \"feat(cmd): add omega-api Connect-RPC server entry point\"\n```\n\n---\n\n## Task 6: Frontend Scaffold + Connect-ES Setup\n\n**Files:**\n- Create: `dashboard/` — Vite React TS project\n\n- [ ] **Step 1: Scaffold Vite project at repo root**\n\n```bash\ncd /Users/benebsworth/projects/omega\nnpm create vite@latest dashboard -- --template react-ts\n```\n\n- [ ] **Step 2: Install dependencies**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm install\nnpm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf\nnpm install react-router-dom recharts lucide-react\nnpm install -D tailwindcss postcss autoprefixer @types/node\nnpx tailwindcss init -p\n```\n\n- [ ] **Step 3: Configure Tailwind — `tailwind.config.js`**\n\n```js\n/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n theme: {\n extend: {\n colors: {\n surface: {\n 900: \"#0f1117\",\n 800: \"#161b27\",\n 700: \"#1e2533\",\n 600: \"#252d3d\",\n },\n },\n },\n },\n plugins: [],\n}\n```\n\n- [ ] **Step 4: Replace `src/index.css`**\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n- [ ] **Step 5: Configure Vite proxy — `vite.config.ts`**\n\n```ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n port: 5173,\n proxy: {\n // Proxy all Connect-RPC calls to the Go server\n \"/omega.v1\": {\n target: \"http://localhost:8080\",\n changeOrigin: true,\n },\n },\n },\n});\n```\n\n- [ ] **Step 6: Write `src/client.ts` — Connect transport + service client**\n\n```typescript\nimport { createConnectTransport } from \"@connectrpc/connect-web\";\nimport { createClient } from \"@connectrpc/connect\";\nimport { OrchestratorService } from \"./gen/omega/v1/omega_service_pb\";\n\n// In dev, Vite proxies /omega.v1/* → http://localhost:8080\n// In production, VITE_API_URL should point to the Go server\nconst baseUrl = import.meta.env.VITE_API_URL ?? \"\";\n\nexport const transport = createConnectTransport({\n baseUrl,\n // Use the Connect protocol (works over HTTP/1.1 without gRPC-web)\n});\n\nexport const client = createClient(OrchestratorService, transport);\n```\n\n- [ ] **Step 7: Commit scaffold**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/\ngit commit -m \"feat(dashboard): scaffold Vite React TS with Connect-ES + Tailwind\"\n```\n\n---\n\n## Task 7: Layout Components\n\n**Files:**\n- Create: `dashboard/src/components/layout/Sidebar.tsx`\n- Create: `dashboard/src/components/layout/Header.tsx`\n- Create: `dashboard/src/App.tsx`\n\n- [ ] **Step 1: Write `Sidebar.tsx`**\n\n```tsx\nimport { NavLink } from \"react-router-dom\";\nimport {\n LayoutDashboard, Server, GitBranch, BarChart2,\n AlertTriangle, Brain, TrendingUp,\n} from \"lucide-react\";\n\nconst NAV = [\n { to: \"/\", icon: LayoutDashboard, label: \"Dashboard\" },\n { to: \"/nodes\", icon: Server, label: \"Nodes\" },\n { to: \"/traces\", icon: GitBranch, label: \"Traces\" },\n { to: \"/metrics\", icon: BarChart2, label: \"Metrics\" },\n { to: \"/issues\", icon: AlertTriangle, label: \"Issues\" },\n { to: \"/memory\", icon: Brain, label: \"Memory\" },\n { to: \"/convergence\", icon: TrendingUp, label: \"Convergence\" },\n];\n\nexport default function Sidebar() {\n return (\n \n );\n}\n```\n\n- [ ] **Step 2: Write `Header.tsx`**\n\n```tsx\nimport { useState } from \"react\";\nimport { client } from \"../../client\";\nimport { Play, Square, Zap } from \"lucide-react\";\n\ninterface HeaderProps {\n systemStatus?: string;\n connected?: boolean;\n}\n\nexport default function Header({ systemStatus, connected }: HeaderProps) {\n const [loading, setLoading] = useState(null);\n\n const statusColor = {\n healthy: \"bg-green-500\",\n degraded: \"bg-yellow-500\",\n critical: \"bg-red-500\",\n no_nodes: \"bg-gray-500\",\n }[systemStatus ?? \"no_nodes\"] ?? \"bg-gray-500\";\n\n async function action(name: \"start\" | \"stop\" | \"trigger\") {\n setLoading(name);\n try {\n if (name === \"start\") await client.startOrchestrator({ heartbeatSecs: 120 });\n else if (name === \"stop\") await client.stopOrchestrator({});\n else await client.triggerHeartbeat({});\n } finally {\n setLoading(null);\n }\n }\n\n return (\n
\n
\n \n \n {systemStatus ?? \"unknown\"}\n \n {connected !== undefined && (\n \n {connected ? \"streaming\" : \"offline\"}\n \n )}\n
\n
\n {([\"start\", \"trigger\", \"stop\"] as const).map((name) => (\n action(name)}\n disabled={!!loading}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors disabled:opacity-50 ${\n name === \"start\" ? \"bg-green-700 hover:bg-green-600\" :\n name === \"trigger\" ? \"bg-indigo-700 hover:bg-indigo-600\" :\n \"bg-red-800 hover:bg-red-700\"\n }`}\n >\n {name === \"start\" ? : name === \"stop\" ? : }\n {name.charAt(0).toUpperCase() + name.slice(1)}\n \n ))}\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Write `App.tsx`**\n\n```tsx\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Sidebar from \"./components/layout/Sidebar\";\nimport Header from \"./components/layout/Header\";\nimport { client } from \"./client\";\nimport type { SystemHealth } from \"./gen/omega/v1/types_pb\";\nimport Dashboard from \"./pages/Dashboard\";\nimport Nodes from \"./pages/Nodes\";\nimport Traces from \"./pages/Traces\";\nimport Metrics from \"./pages/Metrics\";\nimport Issues from \"./pages/Issues\";\nimport Memory from \"./pages/Memory\";\nimport Convergence from \"./pages/Convergence\";\n\nexport default function App() {\n const [health, setHealth] = useState(null);\n const [streamConnected, setStreamConnected] = useState(false);\n const abortRef = useRef(null);\n\n useEffect(() => {\n startStream();\n return () => abortRef.current?.abort();\n }, []);\n\n async function startStream() {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n setStreamConnected(true);\n for await (const event of client.streamEvents(\n { pollIntervalMs: 2000 },\n { signal: controller.signal }\n )) {\n if (event.payload.case === \"healthUpdate\") {\n setHealth(event.payload.value);\n }\n }\n } catch {\n setStreamConnected(false);\n // Reconnect after 3s\n setTimeout(startStream, 3000);\n }\n }\n\n return (\n \n
\n \n
\n \n
\n \n } />\n } />\n } />\n } />\n } />\n } />\n } />\n } />\n \n
\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 4: Commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add dashboard/src/\ngit commit -m \"feat(dashboard): add layout + App router with server-streaming health\"\n```\n\n---\n\n## Task 8: Dashboard Page\n\n**Files:**\n- Create: `dashboard/src/pages/Dashboard.tsx`\n- Create: `dashboard/src/components/NodeCard.tsx`\n\n- [ ] **Step 1: Write `NodeCard.tsx`**\n\n```tsx\nimport { LineChart, Line, ResponsiveContainer } from \"recharts\";\nimport type { Node } from \"../gen/omega/v1/types_pb\";\n\nfunction healthColor(h: number) {\n if (h >= 0.8) return \"border-green-600 bg-green-900/20\";\n if (h >= 0.6) return \"border-yellow-600 bg-yellow-900/20\";\n return \"border-red-600 bg-red-900/20\";\n}\n\nfunction healthText(h: number) {\n if (h >= 0.8) return \"text-green-400\";\n if (h >= 0.6) return \"text-yellow-400\";\n return \"text-red-400\";\n}\n\ninterface NodeCardProps { node: Node }\n\nexport default function NodeCard({ node }: NodeCardProps) {\n const sparkData = node.latencyHistory.slice(-20).map((p) => ({ v: p.durationMs }));\n const lastExecAgo = node.lastExecution?.startedAt\n ? Math.round((Date.now() / 1000 - Number(node.lastExecution.startedAt.seconds)) / 60)\n : null;\n\n return (\n
\n
\n
\n

{node.name}

\n

v{node.version}

\n
\n \n {(node.health * 100).toFixed(0)}%\n \n
\n {sparkData.length > 1 && (\n
\n \n \n = 0.8 ? \"#4ade80\" : node.health >= 0.6 ? \"#facc15\" : \"#f87171\"}\n dot={false} strokeWidth={1.5} />\n \n \n
\n )}\n
\n err: {(node.errorRate * 100).toFixed(1)}%\n {lastExecAgo !== null ? `${lastExecAgo}m ago` : \"never\"}\n
\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Dashboard.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport NodeCard from \"../components/NodeCard\";\nimport type { SystemHealth, Node, ActivityEntry } from \"../gen/omega/v1/types_pb\";\n\ninterface DashboardProps { health: SystemHealth | null }\n\nexport default function Dashboard({ health }: DashboardProps) {\n const [nodes, setNodes] = useState([]);\n const [activity, setActivity] = useState([]);\n\n useEffect(() => {\n client.listNodes({}).then((r) => setNodes(r.nodes)).catch(console.error);\n client.listActivity({ limit: 20 }).then((r) => setActivity(r.entries)).catch(console.error);\n }, []);\n\n const statusColor: Record = {\n healthy: \"text-green-400\",\n degraded: \"text-yellow-400\",\n critical: \"text-red-400\",\n };\n\n return (\n
\n {/* System stats */}\n
\n {[\n { label: \"Status\", value: health?.status ?? \"—\", cls: statusColor[health?.status ?? \"\"] ?? \"text-white\" },\n { label: \"Health Score\", value: health ? `${(health.compositeScore * 100).toFixed(1)}%` : \"—\", cls: \"text-white\" },\n { label: \"Total Cycles\", value: String(health?.totalCycles ?? \"—\"), cls: \"text-white\" },\n { label: \"Open Issues\", value: String(health?.openIssues ?? \"—\"), cls: (health?.openIssues ?? 0) > 0 ? \"text-red-400\" : \"text-white\" },\n ].map(({ label, value, cls }) => (\n
\n

{label}

\n

{value}

\n
\n ))}\n
\n\n {/* Node grid */}\n
\n

\n Nodes ({nodes.length})\n

\n
\n {nodes.map((n) => )}\n
\n {nodes.length === 0 && (\n

\n No nodes registered — run the orchestrator first.\n

\n )}\n
\n\n {/* Activity feed */}\n
\n

\n Recent Activity\n

\n
\n {activity.map((evt) => (\n
\n \n {evt.recordedAt\n ? new Date(Number(evt.recordedAt.seconds) * 1000).toLocaleTimeString()\n : \"—\"}\n \n {evt.actionType}\n {evt.entityId}\n
\n ))}\n {activity.length === 0 && (\n

No activity yet.

\n )}\n
\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/Dashboard.tsx dashboard/src/components/NodeCard.tsx\ngit commit -m \"feat(dashboard): add Dashboard page using Connect-ES typed client\"\n```\n\n---\n\n## Task 9: Traces Page with Waterfall\n\n**Files:**\n- Create: `dashboard/src/components/TraceWaterfall.tsx`\n- Create: `dashboard/src/pages/Traces.tsx`\n\n- [ ] **Step 1: Write `TraceWaterfall.tsx`**\n\n```tsx\nimport type { Span } from \"../gen/omega/v1/types_pb\";\n\ninterface TraceWaterfallProps { spans: Span[] }\n\nexport default function TraceWaterfall({ spans }: TraceWaterfallProps) {\n if (!spans.length) return

No spans.

;\n\n const sorted = [...spans].sort(\n (a, b) => Number(a.startedAt?.seconds ?? 0) - Number(b.startedAt?.seconds ?? 0)\n );\n const minTs = Number(sorted[0]?.startedAt?.seconds ?? 0);\n const maxTs = sorted.reduce(\n (m, s) => Math.max(m, Number(s.endedAt?.seconds ?? s.startedAt?.seconds ?? 0)), 0\n );\n const totalMs = Math.max(1, (maxTs - minTs) * 1000);\n\n const depthMap = new Map();\n for (const span of sorted) {\n depthMap.set(span.spanId, span.parentSpanId ? (depthMap.get(span.parentSpanId) ?? 0) + 1 : 0);\n }\n\n return (\n
\n {sorted.map((span) => {\n const depth = depthMap.get(span.spanId) ?? 0;\n const offsetMs = (Number(span.startedAt?.seconds ?? 0) - minTs) * 1000;\n const durMs = span.durationMs;\n const leftPct = (offsetMs / totalMs) * 100;\n const widthPct = Math.max(0.5, (durMs / totalMs) * 100);\n const isErr = span.status === \"error\";\n\n return (\n
\n
\n {isErr ? \"✗\" : \"✓\"}{\" \"}\n {span.nodeName || span.operation}\n
\n
\n \n
\n
{durMs.toFixed(0)}ms
\n
\n );\n })}\n
\n );\n}\n```\n\n- [ ] **Step 2: Write `Traces.tsx`**\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { client } from \"../client\";\nimport TraceWaterfall from \"../components/TraceWaterfall\";\nimport type { TraceSummary, Span } from \"../gen/omega/v1/types_pb\";\n\nexport default function Traces() {\n const [traces, setTraces] = useState([]);\n const [selected, setSelected] = useState(null);\n const [spans, setSpans] = useState([]);\n\n useEffect(() => {\n client.listTraces({ limit: 30 }).then((r) => setTraces(r.traces)).catch(console.error);\n }, []);\n\n function selectTrace(id: string) {\n setSelected(id);\n client.getTrace({ traceId: id }).then((r) => setSpans(r.spans)).catch(console.error);\n }\n\n return (\n
\n
\n
\n Recent Traces\n
\n {traces.map((t) => (\n \n ))}\n {traces.length === 0 &&

No traces yet

}\n
\n
\n {selected\n ? <>

trace: {selected}

\n :

Select a trace to view the waterfall

\n }\n
\n
\n );\n}\n```\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/components/TraceWaterfall.tsx dashboard/src/pages/Traces.tsx\ngit commit -m \"feat(dashboard): add trace waterfall viewer with typed Connect-ES\"\n```\n\n---\n\n## Task 10: Remaining Pages + Brain Config UI\n\n**Files:**\n- Create: `dashboard/src/pages/Nodes.tsx`\n- Create: `dashboard/src/components/BrainConfigPanel.tsx`\n- Create: `dashboard/src/pages/Metrics.tsx`\n- Create: `dashboard/src/pages/Issues.tsx`\n- Create: `dashboard/src/pages/Memory.tsx`\n- Create: `dashboard/src/pages/Convergence.tsx`\n\n- [ ] **Step 1: Write `BrainConfigPanel.tsx` — model switcher component**\n\n```tsx\nimport { useState, useEffect } from \"react\";\nimport { client } from \"../client\";\nimport type { NodeConfig, ProviderInfo } from \"../gen/omega/v1/types_pb\";\n\ninterface Props { nodeId: string }\n\nexport default function BrainConfigPanel({ nodeId }: Props) {\n const [config, setConfig] = useState(null);\n const [providers, setProviders] = useState([]);\n const [saving, setSaving] = useState(false);\n const [draft, setDraft] = useState({ provider: \"\", model: \"\", temperature: 0.7, maxTokens: 4096 });\n\n useEffect(() => {\n if (!nodeId) return;\n client.getNodeConfig({ nodeId }).then((r) => {\n setConfig(r.config ?? null);\n if (r.config?.brain) {\n const b = r.config.brain;\n setDraft({ provider: b.provider, model: b.model, temperature: b.temperature, maxTokens: b.maxTokens });\n }\n });\n client.listAvailableProviders({}).then((r) => setProviders(r.providers));\n }, [nodeId]);\n\n const currentProvider = providers.find((p) => p.id === draft.provider);\n\n async function save() {\n setSaving(true);\n try {\n const r = await client.updateNodeBrain({\n nodeId,\n brain: { provider: draft.provider, model: draft.model, temperature: draft.temperature, maxTokens: draft.maxTokens, systemPrompt: config?.brain?.systemPrompt ?? \"\", extraConfig: {} },\n });\n setConfig(r.config ?? null);\n } finally {\n setSaving(false);\n }\n }\n\n if (!config) return
Loading brain config…
;\n\n return (\n
\n

\n 🧠 LLM Brain Config\n

\n
\n
\n \n setDraft({ ...draft, provider: e.target.value, model: \"\" })}\n >\n {providers.map((p) => )}\n \n
\n
\n \n setDraft({ ...draft, model: e.target.value })}\n >\n {(currentProvider?.models ?? []).map((m) => )}\n \n
\n
\n \n setDraft({ ...draft, temperature: parseFloat(e.target.value) })}\n />\n
\n
\n \n setDraft({ ...draft, maxTokens: parseInt(e.target.value, 10) })}\n />\n
\n
\n \n {saving ? \"Saving…\" : \"Apply Brain Config\"}\n \n
\n );\n}\n```\n\n- [ ] **Step 2: Write stubs for remaining pages (iterate to full implementations)**\n\nEach page follows the same pattern: call `client.methodName({})`, display results. Write full implementations using the generated types.\n\n`Nodes.tsx` — calls `client.listNodes`, `client.getNode`, `client.getMetricsHistory`; includes `` in the node detail panel\n`Metrics.tsx` — calls `client.getMetricsHistory`, renders per-node `LineChart` with recharts\n`Issues.tsx` — calls `client.listIssues({ stateFilter: \"open\" })`, renders sortable table\n`Memory.tsx` — calls `client.getNodeMemory({ nodeId: \"\" })` for global view, semantic concept list\n`Convergence.tsx` — calls `client.getConvergence`, `client.listImprovements`, renders score chart\n\nThese pages follow the same structure as shown in Tasks 8 and 9. Write them using the generated proto types from `./gen/omega/v1/types_pb`.\n\n**`Nodes.tsx` detail panel pattern:**\n```tsx\n// When a node is selected, show node stats + brain config side by side\n{selectedNode && (\n
\n
\n {/* node stats, latency chart, execution history */}\n
\n \n
\n)}\n```\n\n- [ ] **Step 2: Verify build**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run build 2>&1 | tail -10\n```\n\nExpected: `built in Xs` with no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add dashboard/src/pages/\ngit commit -m \"feat(dashboard): complete all 7 pages\"\n```\n\n---\n\n## Task 11: End-to-End Verification\n\n- [ ] **Step 1: Run one Python heartbeat to populate SQLite DBs**\n\n```bash\ncd /Users/benebsworth/projects/omega\npython -m omega.examples.victoria_main --once\n```\n\nExpected: heartbeat runs, data written to `/tmp/omega_victoria_state.db`.\n\n- [ ] **Step 2: Start Go API server**\n\n```bash\ncd /Users/benebsworth/projects/omega\n./omega-api &\nsleep 1\n```\n\n- [ ] **Step 3: Verify key endpoints**\n\n```bash\n# Health\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetHealth \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Nodes\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool | head -30\n\n# Traces\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListTraces \\\n -H \"Content-Type: application/json\" -d '{\"limit\": 5}' | python3 -m json.tool\n\n# Brain config — list providers\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListAvailableProviders \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -m json.tool\n\n# Brain config — get node config (replace NODE_ID with a real node_id from ListNodes)\nNODE_ID=$(curl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/ListNodes \\\n -H \"Content-Type: application/json\" -d '{}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['nodes'][0]['nodeId'])\" 2>/dev/null)\ncurl -s -X POST http://localhost:8080/omega.v1.OrchestratorService/GetNodeConfig \\\n -H \"Content-Type: application/json\" -d \"{\\\"nodeId\\\": \\\"$NODE_ID\\\"}\" | python3 -m json.tool\n```\n\nExpected: nodes with health scores, at least one trace, providers list with 5 entries, node config defaulting to `claude-sonnet-4-6`.\n\n- [ ] **Step 4: Start frontend**\n\n```bash\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev &\nsleep 2\necho \"Dashboard at http://localhost:5173\"\n```\n\n- [ ] **Step 5: Verify via Playwright MCP**\n\nUse `mcp__plugin_playwright_playwright__browser_navigate` to visit `http://localhost:5173` and take a screenshot to confirm the dashboard loads.\n\n- [ ] **Step 6: Final commit**\n\n```bash\ncd /Users/benebsworth/projects/omega\ngit add -A\ngit commit -m \"feat: complete Omega Go+Connect-RPC API and React dashboard\"\n```\n\n---\n\n## Quick-start (reference)\n\n```bash\n# Terminal 1: Go API\ncd /Users/benebsworth/projects/omega\ngo build ./cmd/omega-api && ./omega-api\n\n# Terminal 2: React dashboard\ncd /Users/benebsworth/projects/omega/dashboard\nnpm run dev\n\n# Terminal 3: Python orchestrator\npython -m omega.examples.victoria_main --heartbeat 60\n\n# Open dashboard\nopen http://localhost:5173\n```\n"}}}}